The check's composer composes a module the running indexer alone holds when the change's manifest is the check's to judge (issue 1921, milestone 47/G H2)

ADR 0578 §2 and §3: a case's accepted verdict, or none (the running checker drops the change's own), lifts the
input's hold on a manifest the composer reads whole; a case's refusal and bytes that do not decode stay held. It is
composed at the build the input carries (Input.Holds). Without that build the node is not judged and the verdict
names the module, why it was held and that its build is awaited; where it cannot compose without it, it is composed
held as its node-engine holds it, and said so. Never a silent pass (the lead's Q4 ruling). The issue 774 row now
gives the runner the refused verdict the checker gives a manifest the change does not bring.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-07 02:33:21 +01:00
parent c114b786e8
commit d0fb384f00
2 changed files with 213 additions and 5 deletions
+137 -3
View File
@@ -15,6 +15,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"maps"
"slices" "slices"
"sort" "sort"
"strings" "strings"
@@ -162,6 +163,8 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
if err != nil { if err != nil {
return CaseVerdict{}, err return CaseVerdict{}, err
} }
held := base
base, awaited := overrule(base, c, cv.Manifests, shelf)
assignments := map[string]*meshmoduleassigner.Assignments{} assignments := map[string]*meshmoduleassigner.Assignments{}
for node, raw := range c.Assignments { for node, raw := range c.Assignments {
if string(raw) == "null" { if string(raw) == "null" {
@@ -189,9 +192,17 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
shelf[name] = m shelf[name] = m
} }
// A module the input carries as its node-engine took it in from its build (SentInput) is composed so: the bus // A module the input carries as its node-engine took it in from its build (SentInput) is composed so: the bus
// reader composes it from its build, never from the indexer's manifest a case gives. // reader composes it from its build, never from the indexer's manifest a case gives. One the case overrules the
// node's hold on is composed at the build the node would compose it at (Input.Holds), else from the case's manifest.
for name, m := range base.Catalogue { for name, m := range base.Catalogue {
shelf[name] = m if _, over := awaited[name]; !over {
shelf[name] = m
}
}
for name, hold := range base.Holds {
if _, over := awaited[name]; over && hold.Built != nil {
shelf[name] = *hold.Built
}
} }
// A module the input's node-engine holds as refused (compose.Input.Refused: its verdict, or bytes that do not decode) is // A module the input's node-engine holds as refused (compose.Input.Refused: its verdict, or bytes that do not decode) is
// held here as there, and its read says why, whatever the case's own verdicts: the check then names the cause the node // held here as there, and its read says why, whatever the case's own verdicts: the check then names the cause the node
@@ -225,11 +236,134 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
} }
cv.Machines = map[string]Machine{} cv.Machines = map[string]Machine{}
for _, node := range r.Machines { for _, node := range r.Machines {
cv.Machines[node] = machine(in, node, depth, r.Declarations) m := machine(in, node, depth, r.Declarations)
if words := awaitedOn(in, node, awaited); words != "" {
// Composed without the build it runs at: named per node and module, never a silent pass (issue 1921). The
// machine is then not judged, as for a node whose running state its engine does not send (ADR 0593 §9). Where
// it does not compose without that build (a tool runner's bundle), it is composed with the module held as its
// node-engine holds it, and named so; a node that refuses then too is the change's refusal.
how := "composed from the change's manifest without its build: "
if !m.Composes && m.NotJudged == "" {
if again := machine(heldAgain(in, held, awaited), node, depth, r.Declarations); again.Composes {
m, how = again, "composed with it held as its node-engine holds it, since it does not compose without its build: "
}
}
words = how + words
if m.Composes {
m = Machine{NotJudged: words, Declared: m.Declared}
} else if m.NotJudged == "" {
m.Reason += "; " + words
}
}
cv.Machines[node] = m
} }
return cv, nil return cv, nil
} }
// overrule lifts the hold of the input's node-engine on each module whose manifest the case carries and judges itself
// (novox/hq ADR 0578 §2): a module the input holds (Refused: the running indexer's verdict, or the running node-engine's
// decoder), whose bytes this composer reads whole, and that the case gives an accepted verdict or none (a reader takes a
// manifest with no verdict as accepted, ADR 0578 §3; the running checker drops the verdicts of the change's manifests,
// issue 1921). It is then no longer Refused nor held in how the node composes. A case's own refusal and bytes this
// composer does not decode stay held. awaited is each module lifted with no build the input carries: the words for the verdict, or
// "" where it is composed at its build.
func overrule(base compose.Input, c Case, reads map[string]ManifestRead, shelf map[string]catalogue.Manifest) (compose.Input, map[string]string) {
awaited := map[string]string{}
for name, why := range base.Refused {
hold := base.Holds[name]
if _, carried := c.Manifests[name]; !carried || !reads[name].Accepted || shelf[name].ReadNoFurther() {
continue
}
held := "held by the running node-engine"
switch hold.By {
case compose.HeldByVerdict:
held = "refused only by the running indexer"
case compose.HeldByDecoder:
held = "not read by the running node-engine's decoder"
}
judged := "accepted by the check's own validator"
if _, given := c.Verdicts[name]; !given {
judged = "given no verdict by the check, so taken as accepted (novox/hq ADR 0578 §3)"
}
awaited[name] = ""
if hold.Built == nil && compose.NamesBuild(shelf[name]) {
build := "its build"
if hold.Build != "" {
build += " " + hold.Build
}
awaited[name] = fmt.Sprintf("%s is %s (%s) and %s, and %s is awaited", name, held, why, judged, build)
}
}
if len(awaited) == 0 {
return base, awaited
}
base.Refused = maps.Clone(base.Refused)
for name := range awaited {
delete(base.Refused, name)
}
if how := base.ComposedHow; how != nil {
copied := *how
copied.Held = maps.Clone(how.Held)
for name := range awaited {
delete(copied.Held, name)
}
base.ComposedHow = &copied
}
return base, awaited
}
// heldAgain is in with each module overrule lifted and gave no build held again as held, the input before overrule,
// holds it: its stub, Refused and how the node composes.
func heldAgain(in, held compose.Input, awaited map[string]string) compose.Input {
in.Catalogue = maps.Clone(in.Catalogue)
in.Refused = maps.Clone(in.Refused)
if in.Refused == nil {
in.Refused = map[string]string{}
}
if how := in.ComposedHow; how != nil {
copied := *how
copied.Held = maps.Clone(how.Held)
in.ComposedHow = &copied
}
for name, words := range awaited {
if words == "" {
continue
}
if stub, ok := held.Catalogue[name]; ok {
in.Catalogue[name] = stub
} else {
in.Catalogue[name] = compose.Stub(in.Catalogue[name])
}
in.Refused[name] = held.Refused[name]
if in.ComposedHow != nil && held.ComposedHow != nil {
if why, was := held.ComposedHow.Held[name]; was {
if in.ComposedHow.Held == nil {
in.ComposedHow.Held = map[string]string{}
}
in.ComposedHow.Held[name] = why
}
}
}
return in
}
// awaitedOn is the words of each module placed on node that is composed without its build (overrule), joined, sorted.
func awaitedOn(in compose.Input, node string, awaited map[string]string) string {
var out []string
for _, n := range in.Nodes {
if n.Name != node {
continue
}
for _, m := range n.Assigned {
if words := awaited[m]; words != "" {
out = append(out, words)
}
}
}
sort.Strings(out)
return strings.Join(out, "; ")
}
// HoldingsOf is every seat an assigned module claims, held on the node it is assigned to, in node order: the holdings // HoldingsOf is every seat an assigned module claims, held on the node it is assigned to, in node order: the holdings
// of an input that has none of record (the assigner's, mesh-delivery-assigner internal/assign input). // of an input that has none of record (the assigner's, mesh-delivery-assigner internal/assign input).
func HoldingsOf(shelf map[string]catalogue.Manifest, nodes []compose.Node) []catalogue.Held { func HoldingsOf(shelf map[string]catalogue.Manifest, nodes []compose.Node) []catalogue.Held {
+76 -2
View File
@@ -188,7 +188,8 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
}) })
// Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's // Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's
// verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses // verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses
// it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way. // it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way. The
// case gives the runner its refused verdict, as the checker does for a manifest the change does not bring.
t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) { t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) {
raw := `{"module": "runner", "version": "1", "tool-runner": true, ` + raw := `{"module": "runner", "version": "1", "tool-runner": true, ` +
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}` `"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
@@ -211,7 +212,9 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} { for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} {
var r Request var r Request
r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}}}}} Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}},
Verdicts: bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"runner": {V: 1, Module: "runner",
State: meshcatalogclient.VerdictRefused, Problems: []string{"p"}}})}}}
if err := json.Unmarshal([]byte(sent), &r.Input); err != nil { if err := json.Unmarshal([]byte(sent), &r.Input); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -227,6 +230,77 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
} }
} }
}) })
// ADR 0578 §2, §3 (issue 1921): a module its node-engine holds by the running indexer's verdict gives way when the case,
// judging the change's manifest itself, gives it an accepted verdict or none (the running checker drops the change's
// own), and this composer reads its bytes whole. It is composed at the build the input carries for it (Input.Holds);
// without one it is composed from the case's manifest and the node is not judged, naming it (never a silent pass), or,
// where it cannot compose without its build (a tool runner's bundle), composed held as its node-engine holds it, named
// so. Bytes that do not decode here, and a case's own refusal, stay held.
t.Run("a held module the case judges itself", func(t *testing.T) {
app := `{"module": "app", "version": "1", "resources": [{"id": "server", "type": "container", "name": "app-server", "artifact": "server"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
// A module that names a build and composes from its manifest without it.
plain := `{"module": "app", "version": "1", "resources": [{"id": "d", "type": "directory", "path": "/var/lib/app"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
runner := `{"module": "app", "version": "1", "tool-runner": true, ` +
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
built, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", "resources": [{"id": "built", "type": "directory", "path": "/var/lib/app-built"}]}`))
if err != nil {
t.Fatal(err)
}
verdict := func(state string) map[string]json.RawMessage {
return bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"app": {V: 1, Module: "app", State: state, Problems: []string{"p"}}})
}
const why = "the indexer's judge refused the manifest of app"
for _, row := range []struct {
name string
raw string
verdicts map[string]json.RawMessage
hold *compose.Hold
composes bool // the machine composes, judged
path string // a path declared on it
words string // in NotJudged, else in the read's refusal
}{
{"an accepted verdict, its build carried", app, verdict(meshcatalogclient.VerdictAccepted),
&compose.Hold{By: compose.HeldByVerdict, Build: "fp1", Built: &built}, true, "/var/lib/app-built", ""},
{"no verdict, its build carried", app, nil, &compose.Hold{By: compose.HeldByVerdict, Build: "fp1", Built: &built}, true, "/var/lib/app-built", ""},
{"an accepted verdict, its build awaited", plain, verdict(meshcatalogclient.VerdictAccepted), &compose.Hold{By: compose.HeldByVerdict, Build: "fp1"}, false, "",
"composed from the change's manifest without its build: app is refused only by the running indexer (" + why + ") and accepted by the check's own validator, and its build fp1 is awaited"},
{"no verdict, from an engine before holds", plain, nil, nil, false, "",
"app is held by the running node-engine (" + why + ") and given no verdict by the check, so taken as accepted"},
{"a container, its image awaited", app, nil, &compose.Hold{By: compose.HeldByVerdict}, false, "",
"composed with it held as its node-engine holds it, since it does not compose without its build: app is refused only by the running indexer"},
{"a tool runner, its bundle awaited", runner, nil, &compose.Hold{By: compose.HeldByVerdict}, false, "",
"composed with it held as its node-engine holds it, since it does not compose without its build: app is refused only by the running indexer"},
{"bytes that do not decode", `{"module": "app", "version": "1", "resources": "none"}`, nil, &compose.Hold{By: compose.HeldByVerdict, Built: &built}, true, "", "does not decode"},
{"the case's own refusal", app, verdict(meshcatalogclient.VerdictRefused), &compose.Hold{By: compose.HeldByVerdict, Built: &built}, true, "", "refused the manifest of app"},
} {
man, _ := catalogue.Decode([]byte(app))
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"app": compose.Stub(man)}, Refused: map[string]string{"app": why},
Nodes: []compose.Node{{Name: "a", Assigned: []string{"app"}}}}
if row.hold != nil {
in.Holds = map[string]compose.Hold{"app": *row.hold}
}
v, err := Run(withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Declarations: true,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"app": {Raw: json.RawMessage(row.raw)}}, Verdicts: row.verdicts}}}, in))
if err != nil {
t.Fatal(err)
}
m, read := v.Cases[0].Machines["a"], v.Cases[0].Manifests["app"]
declared := false
for _, p := range m.Declared {
declared = declared || p.Path == row.path
}
switch {
case m.Composes != row.composes || (row.path != "" && !declared):
t.Errorf("%s: machine %+v", row.name, m)
case !row.composes && (m.NotJudged == "" || !strings.Contains(m.NotJudged, row.words)):
t.Errorf("%s: not judged %q, want it naming %q", row.name, m.NotJudged, row.words)
case row.composes && row.path == "" && (read.Accepted || !strings.Contains(read.Refused, row.words)):
t.Errorf("%s: read %+v, want held naming %q", row.name, read, row.words)
}
}
})
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}) v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" { if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
t.Fatalf("a request with no machines: %+v, %v", v, err) t.Fatalf("a request with no machines: %+v, %v", v, err)