diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index 6251e35..15c33bc 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -218,6 +218,26 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.Fp2, err, read.ComposedHow, raw) } + // Issue 1921: held by its manifest's verdict, spiegel is still held in what the node composes, and the input a check + // is answered with says what holds it and carries the build the node would compose it at, taken in while done; a + // node's own read takes in no build of it. + sp.Verdict, sp.Problems = meshcatalogclient.VerdictRefused, []string{"p"} + bb.modules["spiegel"] = sp + for _, done := range []bool{true, false} { + if !done { + bb.builds["spiegel "+sp.Fp2].State = builds.BuildBuilding + } + read, err = ComposingInput(ctx, bb, Mesh{}, Scope{Node: "n1"}, map[string]string{"lemurs": fp("e"), "spiegel": sp.Fp2}) + hold := read.Holds["spiegel"] + raw, _ = json.Marshal(hold.Built) + if err != nil || !read.Catalogue["spiegel"].Stubbed || hold.By != compose.HeldByVerdict || hold.Build != sp.Fp2 || + strings.Contains(string(raw), strings.Repeat("7", 64)) != done || (hold.Built == nil) == done { + t.Errorf("spiegel refused, its build done %v: %v, hold %+v (built %s)", done, err, hold, raw) + } + } + if own, err := Input(ctx, bb, Mesh{}, Scope{Node: "n1"}); err != nil || own.Holds["spiegel"] != (compose.Hold{By: compose.HeldByVerdict}) { + t.Errorf("a node's own read of spiegel refused: %v, hold %+v", err, own.Holds["spiegel"]) + } } // (C) Issue 1126, C5: what the others read of a module carried as it runs is what runs, never its stub's: the filter diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index f83a051..7aabec4 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -167,6 +167,10 @@ type Scope struct { // Kept is told, in words, each module composed as what runs shows because its manifest does not yet say it (the tool // runner, the core's credentials; ADR 0601 §4), so the node says it. Nil: said nowhere. Kept func(module, words string) + // takeInHeld, set for the input a node answers a check with (ComposingInput), takes in the build of each module placed + // on the node that its manifest's verdict or its decoder holds, into compose.Input.Holds, never into what the node composes: the + // check's composer, judging the change's manifest itself, composes it at that build (issue 1921). + takeInHeld bool } // InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs @@ -214,21 +218,23 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er // taken is a module's manifest as a reader takes it (novox/hq ADR 0578 §3): decoded leniently, with the indexer's verdict // from its modules value. A refused one, or bytes that do not decode, stands as its stub (unusable) and why says so; a // manifest not read past a field it set aside holds its name alone and is held the same. -func taken(name string, raw []byte, m meshcatalog.ModulesByModuleValue) (man catalogue.Manifest, why string) { +// +// by is what holds it (compose.HeldByVerdict or compose.HeldByDecoder), empty when nothing does. +func taken(name string, raw []byte, m meshcatalog.ModulesByModuleValue) (man catalogue.Manifest, why, by string) { if v := meshcatalogclient.ModuleVerdict(name, m); v != nil { if refused, _ := meshcatalogclient.TakeVerdict(v); refused { - return unusable(name, raw), meshcatalogclient.VerdictWhy(*v) + return unusable(name, raw), meshcatalogclient.VerdictWhy(*v), compose.HeldByVerdict } } man, err := catalogue.Decode(raw) switch { case err != nil: - return unusable(name, raw), fmt.Sprintf("the manifest of %s does not decode: %v", name, err) + return unusable(name, raw), fmt.Sprintf("the manifest of %s does not decode: %v", name, err), compose.HeldByDecoder case man.ReadNoFurther(): return unusable(name, raw), fmt.Sprintf("the manifest of %s could not be read past a field this reader does not "+ - "know: %s", name, man.SetAside()) + "know: %s", name, man.SetAside()), compose.HeldByDecoder } - return man, "" + return man, "", "" } // input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in, or whose @@ -374,9 +380,13 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) { // follows (issue 774: power's bar item refused every node's composition, 2026-10-30; ADR 0578 §3: one refused // manifest silenced disk-load for 45 hours). It stands in as its stub, so what it provides and requires still // shapes the rest; it is held on the composing node where it is placed there, and elsewhere only its offer is read. - man, why := taken(name, raw, m) + man, why, by := taken(name, raw, m) if why != "" { named(&in.Refused, name, why) + if in.Holds == nil { + in.Holds = map[string]compose.Hold{} + } + in.Holds[name] = compose.Hold{By: by} if ownPlaced[name] { b.ownRefused = append(b.ownRefused, [2]string{name, why}) } @@ -600,6 +610,7 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s // credential: the sandbox composes no pair value. func ComposingInput(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (compose.Input, error) { held := map[string]string{} + s.takeInHeld = true in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, fps) if err != nil { return compose.Input{}, err @@ -909,7 +920,23 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m continue } if _, refused := in.Refused[pm.Module]; refused { - continue // held as its stub: no build of a refused manifest is taken in (ADR 0578 §3) + // Held as its stub: no build of a refused manifest is composed (ADR 0578 §3). For a check, the build the node + // would compose it at is said beside it, and taken in when done, for the check's composer alone. + if hold, said := in.Holds[pm.Module]; s.takeInHeld && said { + hold.Build = m.Fp2 + if w := wanted[pm.Module]; w != "" { + hold.Build = w + } + // Its stub names no build, so the builder is asked: a build not done, or not taken in, is awaited. + if b, err := build(ctx, pm.Module, hold.Build); err == nil && b != nil && b.State == builds.BuildDone { + if resolved, err := built(pm.Module, hold.Build, b); err == nil { + hold.Built = &resolved + } + } + in.Holds = maps.Clone(in.Holds) + in.Holds[pm.Module] = hold + } + continue } if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) { continue // nothing is built of it: its manifest is what it composes from diff --git a/internal/compose/input.go b/internal/compose/input.go index e1eaa9b..74c7c8d 100644 --- a/internal/compose/input.go +++ b/internal/compose/input.go @@ -114,11 +114,36 @@ type Input struct { // (compose.Stub), so it is held: nothing of its own is composed, while what it provides and requires still shapes the // rest. A module not named is accepted, a missing verdict included. Refused map[string]string `json:",omitempty"` + // Holds is, by module Refused names, what holds it and the build its node would compose it at (issue 1921): a check's + // composer that judges the change's manifest itself (novox/hq ADR 0578 §2) composes a module refused by the running + // indexer alone from that build, and names one whose build it is not given. Nil from a node-engine before it. + Holds map[string]Hold `json:",omitempty"` // SetAside is each module whose manifest has a field the reader's decoder does not know, by module, with what was set // aside (catalogue.Manifest.SetAside): the rest of it is composed, and the reader names it (ADR 0578 §6). SetAside map[string]string `json:",omitempty"` } +// What holds a module Refused names (Hold.By). +const ( + // HeldByVerdict is a module the indexer's judge refused the manifest of (its verdict). + HeldByVerdict = "verdict" + // HeldByDecoder is a module whose bytes this reader's decoder does not decode, or cannot read past a field it does not + // know: no judge of the manifest overrules it. + HeldByDecoder = "decoder" +) + +// Hold is what holds one module Refused names, and the build its node would compose it at (Input.Holds). +type Hold struct { + // By is HeldByVerdict or HeldByDecoder. + By string + // Build is the fingerprint the node would compose it at, its want's or else the indexer's newest; empty where it is not + // placed on the composing node or names no build. + Build string `json:",omitempty"` + // Built is its manifest as the node takes it in from that build, when the builder holds it done; nil while that build + // is awaited (or not asked: only a node's answer to a check asks it). + Built *catalogue.Manifest `json:",omitempty"` +} + // Issued is one pair credential made for a consumer of a provider (inv.Secret): ForProvider is its copy sealed to the // provider, optional. type Issued struct {