A held module is in status, and it stops the mesh reading as well

novox/hq 04-ISSUES/125. A module assigned to a machine and never taken
runs none of what it declares. Status had no vocabulary for it: the
machine was heard from, current, and doing what it was told, so the mesh
printed "all doing what they were told" — which was true, and was acted
on, and every public name on the machine went dark.

Status now names each module a machine is holding rather than running,
per machine and with a count, read from what the MACHINE reported rather
than from the mesh's take-time listing — the machine is the only thing
that knows what it found. The JSON form carries the same rows, absent
rather than empty when nothing is held.

And a hold suppresses the all-well sentence, where being adopted does
not: adopted is a mode somebody chose, a module assigned and never taken
is a half-finished action with nothing left to finish it. The condition
is now a named function so the rule lives in one place and a test binds
to the real thing rather than a copy of it.

untakenModules raises a read it cannot make rather than answering "holding
nothing" from a failed query, which is the shape this whole issue is.
This commit is contained in:
2026-09-30 08:46:20 +02:00
parent e7da39de57
commit bfd983e3f8
4 changed files with 256 additions and 2 deletions
+90 -2
View File
@@ -171,6 +171,39 @@ func statusCommand(ctx context.Context, args []string) error {
fmt.Printf("\n `push --behind` sends them\n\n")
}
if len(asked.untaken) > 0 {
// **Before the adopted line, and it breaks "all well".** An adopted machine is a state
// somebody chose and can leave alone; a module assigned to one and never taken is work
// outstanding that reads exactly like work finished. That reading is what stopped a
// predecessor's proxy on the strength of four green surfaces (novox/hq 04-ISSUES/125).
machines := make([]string, 0, len(asked.untaken))
for name := range asked.untaken {
machines = append(machines, name)
}
sort.Strings(machines)
total := 0
for _, held := range asked.untaken {
for _, n := range held {
total += n
}
}
fmt.Printf("%d resource(s) are held as found, because their module was assigned and never "+
"taken — so it is running none of what it declares:\n", total)
for _, name := range machines {
modules := make([]string, 0, len(asked.untaken[name]))
for m := range asked.untaken[name] {
modules = append(modules, m)
}
sort.Strings(modules)
parts := make([]string, 0, len(modules))
for _, m := range modules {
parts = append(parts, fmt.Sprintf("%s (%d)", m, asked.untaken[name][m]))
}
fmt.Printf(" %-12s %s\n", name, strings.Join(parts, ", "))
}
fmt.Printf("\n `take <node> <module>` compares what runs against what it declares, and runs it\n\n")
}
if adopted := adoptedNodes(nodes); len(adopted) > 0 {
// Said, because nothing forces the flip: a node left adopted is visible here rather than
// read as converged (novox/hq ADR 0100). Not a fault, so it does not break "all well".
@@ -178,8 +211,7 @@ func statusCommand(ctx context.Context, args []string) error {
fmt.Printf("\n `converge <node>` previews the flip\n\n")
}
if len(wrong) == 0 && len(quiet) == 0 && len(behind) == 0 && len(asked.waiting) == 0 &&
len(asked.refused) == 0 && asked.network == "" {
if asked.well() {
// Said plainly. "Nothing to report" and "nothing was checked" must never look the same,
// and getting here means every question was asked and answered.
fmt.Printf("%d machine(s), all doing what they were told, all heard from, running what "+
@@ -247,6 +279,13 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) {
if err != nil {
return answers{}, err
}
// And what each machine is holding rather than running, by the module that would run it. Read
// from what the machine itself last reported, not from what take-time computed: the machine is
// the only thing that knows what it found (novox/hq 04-ISSUES/125).
out.untaken, err = untakenModules(ctx, inv, out.nodes)
if err != nil {
return answers{}, err
}
// And which machines are not running what the mesh would send them. The same question as a
// module being behind its source, one level down: that one says the catalogue is out of date,
@@ -281,3 +320,52 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) {
}
return out, nil
}
// untakenModules is, per machine, each module whose resources that machine is holding as found, and
// how many.
//
// **The machine's own account, not the mesh's.** An adopted node decides at apply time what it found
// and reports it; the mesh's take-time listing is a different thing and was the one this command used
// to have, which is why a module assigned after the listing showed nothing at all
// (novox/hq 04-ISSUES/125).
//
// A machine that reports no holds contributes nothing, so a converged mesh answers an empty map and
// the caller prints nothing.
func untakenModules(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) (
map[string]map[string]int, error) {
out := map[string]map[string]int{}
for _, n := range nodes {
said, err := inv.AdoptionOf(ctx, n.Name)
if err != nil {
// A machine whose record cannot be read is not a machine holding nothing. Said, because
// answering "nothing held" from a failed read is the shape this whole issue is about.
return nil, fmt.Errorf("what %s is holding cannot be read: %w", n.Name, err)
}
for _, h := range said.Held {
if h.Module == "" {
continue // a hold the mesh cannot attribute to a module has nothing to take
}
if out[n.Name] == nil {
out[n.Name] = map[string]int{}
}
out[n.Name][h.Module]++
}
}
return out, nil
}
// well is whether every question this command asks came back with nothing to say.
//
// Named, and in one place, because it is the sentence an operator acts on and it has been wrong
// twice. It is deliberately NOT "nothing is broken": a machine holding what it found is not broken
// and is not doing what it was told either.
//
// **A hold suppresses it; being adopted does not.** Adopted is a mode somebody chose and can leave
// alone. A module assigned to a machine and never taken is a half-finished action with nothing left
// to finish it — it runs none of what it declares, and "all doing what they were told" was true and
// read as success for the whole of the edge cut-over outage (novox/hq 04-ISSUES/125).
func (a answers) well() bool {
return len(a.wrong) == 0 && len(a.quiet) == 0 && len(a.behind) == 0 &&
len(a.waiting) == 0 && len(a.refused) == 0 && a.network == "" && len(a.untaken) == 0
}