package main import ( "context" "fmt" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) // Under a module's own `record`, a move that recreates no container and moves no data rolls out (novox/hq ADR // 0377, changing ADR 0242 §1 and ADR 0360 §1). // // A module records because its manifest says so, because it keeps irreplaceable data, because a person set it, or // because it provides the bus. The first two hold for a reason the manifest shows — a change that recreates mail's // containers interrupts everyone's mail, a new database image may upgrade its databases in place — and that hold // also held the moves that interrupt nothing: on 2026-10-15 the database server moved with only its tools bundle // changed, its walk said it "recreates none of its containers", and ace stayed behind until a person pushed. // // So **a move under a module's own `record` is judged per machine, against the build that machine was last sent** // (§2): when it recreates none of the module's containers and moves none of its data (catalogue.Holds, §3) it // goes as a module that rolls out goes — a walk sends it, and every other send treats it as a move waiting for its // walk — and otherwise it is kept at the build the machine runs, as before. The module's policy stays `record`. A // move that cannot be judged waits (§4); a person's `record` and the bus hold every move (§5); and each walk says // per machine which way a recorded module went (§6). // recordedRollsWhy is what a walk says of a recorded module's move that went out (ADR 0377 §6). const recordedRollsWhy = "went out: it recreates none of its containers and moves none of its data (novox/hq ADR 0377)" // recordVerdict is one machine's move of a recorded module: whether it rolls out, and what the walk says of it. type recordVerdict struct { rolls bool // said is what the walk says: recordedRollsWhy, or "waits: " and why — the recreate words, the data change // named, the person's or the bus's hold, or that it could not be judged. said string } // recordedMove judges a module's move on a machine from the build it was last sent (`was`) to the one it would be // sent now. A module that rolls out rolls; one whose policy is a person's or the bus's waits, said; one under its // own `record` rolls when the move recreates nothing and moves no data, and waits when it does or cannot be judged. func (f moveFacts) recordedMove(module, was string) recordVerdict { now := f.current[module] switch { case now.RollOut: return recordVerdict{rolls: true} case now.From == catalogue.FromPerson: return recordVerdict{said: "waits: a person set it to record, and holds every move of it until they push (novox/hq ADR 0377 §5)"} case now.From == catalogue.FromBus: return recordVerdict{said: "waits: it provides the bus, which moves only by the bus step a person starts (novox/hq ADR 0377 §5)"} case !now.OwnRecord(): return recordVerdict{said: "waits: its policy records"} case f.judge == nil: return recordVerdict{said: "waits: it could not be judged — the build records were not read (novox/hq ADR 0377 §4)"} } return f.judge(module, was, now.Commit) } // rolls is whether a module's move on a machine from `was` goes as a move that rolls out (ADR 0377 §1). func (f moveFacts) rolls(module, was string) bool { return f.recordedMove(module, was).rolls } // recordJudge is the judging of moves under a module's own `record`, reading the build records once per move and // the catalogue once (ADR 0377 §2–§4). func recordJudge(ctx context.Context, inv *inventory.Inventory) func(module, was, now string) recordVerdict { judged := map[string]recordVerdict{} var shelf map[string]catalogue.Manifest var shelfErr error shelfRead := false return func(module, was, now string) recordVerdict { key := module + "\x00" + was + "\x00" + now if v, seen := judged[key]; seen { return v } v := func() recordVerdict { cannot := func(why string) recordVerdict { return recordVerdict{said: "waits: it could not be judged — " + why + " (novox/hq ADR 0377 §4)"} } if was == "" { return cannot("what the machine was last sent of it is not known") } from, found, err := inv.ManifestAt(ctx, module, was) if err != nil { return cannot(fmt.Sprintf("its build %s, which the machine was last sent, cannot be read: %v", short(was), err)) } if !found { return cannot(fmt.Sprintf("its build %s, which the machine was last sent, is not in the build records", short(was))) } if !shelfRead { shelf, shelfErr = inv.Catalogue(ctx) shelfRead = true } if shelfErr != nil { return cannot(fmt.Sprintf("the build it would be sent cannot be read: %v", shelfErr)) } to, held := shelf[module] if !held { return cannot("the build it would be sent is not in the catalogue") } if why := catalogue.Holds(from, to); why != "" { return recordVerdict{said: "waits: " + why} } return recordVerdict{rolls: true, said: recordedRollsWhy} }() judged[key] = v return v } } // judgeRecordedInWalk judges, for a walk carrying a module whose policy records, each machine running it whose // build would move (ADR 0377 §2), keeps on the plan which way each went (§6), says it on the controller's log, and // answers the machines its move goes out to. Judged once per walk: a walk read again answers what it kept. func judgeRecordedInWalk(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan, module string, state *inventory.PlanModule) ([]string, error) { if state.Recorded != nil { return wentOut(state.Recorded), nil } running, err := inv.Running(ctx, module) if err != nil { return nil, err } f, err := readMoveFacts(ctx, inv) if err != nil { return nil, err } now := f.current[module] said := map[string]string{} for _, n := range running { sent, known, err := inv.SentBuilds(ctx, n) if err != nil { return nil, err } was, carried := sent[module] if known && carried && f.identical(module, was, now.Commit) { continue // no move there } v := f.recordedMove(module, was) if !known { v = recordVerdict{said: "waits: it could not be judged — what " + n + " was last sent is not known (novox/hq ADR 0377 §4)"} } said[n] = v.said fmt.Printf("%s: %s on %s %s\n", p.ID, module, n, v.said) } if len(said) > 0 { state.Recorded = said } return wentOut(said), nil } // wentOut is the machines a recorded module's move went out to, sorted. func wentOut(recorded map[string]string) []string { var out []string for _, n := range sortedKeysOf(recorded) { if recorded[n] == recordedRollsWhy { out = append(out, n) } } return out } // recordedSaid is, for a walk's note at its end, which way each recorded module it carried went on each machine // (ADR 0377 §6). func recordedSaid(p inventory.Plan) []string { var out []string for _, m := range sortedKeysOf(planModuleNames(p)) { s := p.Modules[m] for _, n := range sortedKeysOf(s.Recorded) { out = append(out, fmt.Sprintf("%s on %s %s", m, n, s.Recorded[n])) } } return out } func planModuleNames(p inventory.Plan) map[string]string { out := map[string]string{} for m, s := range p.Modules { if s != nil && len(s.Recorded) > 0 { out[m] = m } } return out } // heldMove is one move of a recorded module that waits for a person on one machine (ADR 0377 §6). type heldMove struct { Module string From, To string // Why is the walk's own word for it: "waits: " and the recreate words, the data change, or the hold. Why string // Chosen is whether a person's own choice holds it — a module they set to record, or the bus's step — which the // operator is not asked about again: they chose that hold, and the bus has its own condition (S17). Chosen bool } // heldMovesOn is, on one machine, every move of a recorded module that waits for a person, in module order. func heldMovesOn(ctx context.Context, inv *inventory.Inventory, f moveFacts, node string) ([]heldMove, error) { sent, known, err := inv.SentBuilds(ctx, node) if err != nil || !known { return nil, err } var out []heldMove for _, m := range sortedKeysOf(sent) { was := sent[m] now, held := f.current[m] if !held || now.RollOut || now.Commit == "" || was == "" || f.identical(m, was, now.Commit) { continue } if v := f.recordedMove(m, was); !v.rolls { out = append(out, heldMove{Module: m, From: was, To: now.Commit, Why: v.said, Chosen: now.From == catalogue.FromPerson || now.From == catalogue.FromBus}) } } return out, nil } // recordedWaiting is, on one machine, every move of a recorded module that waits for a person, each said with why // (ADR 0377 §6): what `status` names the machine behind for, with `push ` as the remedy. A move under a // module's own `record` that recreates nothing and moves no data is not among them: it goes with its walk. func recordedWaiting(ctx context.Context, inv *inventory.Inventory, f moveFacts, node string) ([]string, error) { moves, err := heldMovesOn(ctx, inv, f, node) var out []string for _, h := range moves { out = append(out, fmt.Sprintf("%s %s → %s %s", h.Module, short(h.From), short(h.To), h.Why)) } return out, err } // stringsKeys is a map of lists as a map of its keys, for sortedKeysOf. func stringsKeys(m map[string][]string) map[string]string { out := map[string]string{} for k := range m { out[k] = k } return out }