A build names its needs and every form of an artifact (ADR 0618 §1, ADR 0624; issue 774)

BuilderBuild.Needs (form:<name>, done keys only) and BuiltArtifact.References (Reference among them, each checked as Reference is) on the delivery layouts mesh-sdk keeps until that area moves (ADR 0613 §3). The node-engine seat's verb and event of ADR 0618 go into mesh-contracts with the seat, not here (ADR 0613 §5).

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 12:40:03 +01:00
parent 3ff694e425
commit 1027803e1d
2 changed files with 49 additions and 8 deletions
+25
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"slices"
"strings"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
@@ -89,6 +90,10 @@ type BuiltArtifact struct {
Kind string `json:"kind,omitempty"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
// References are every form this artifact is named in, Reference among them (novox/hq ADR 0618, ADR 0624): a build
// keeps each form that a counted node-engine states, away nodes included, and Reference stays the form every one
// of them reads. A reader that does not know the field reads Reference alone.
References []string `json:"references,omitempty"`
}
const artifactStore = "artifact-store://"
@@ -211,6 +216,10 @@ type BuilderBuild struct {
// Imports are, on a done key, the mesh packages the built programs import, as the build module answered them
// (BuildAnswer.Imports; novox/hq ADR 0593 §2): the builder carries them onto the build's end (BuildLogEntry.Imports).
Imports []string `json:"imports,omitempty"`
// Needs are, on a done key only, the reader capabilities (`form:<name>`) the build's reference forms need of a
// node-engine, beside the ones its manifest needs (novox/hq ADR 0618 §1). A reader that does not know the field
// reads the build as needing none.
Needs []string `json:"needs,omitempty"`
}
func (b BuilderBuild) check() error {
@@ -245,6 +254,22 @@ func (b BuilderBuild) check() error {
if p := a.referenceProblem(); p != "" {
return fmt.Errorf("delivery: the reference %q of the %s %q %s", a.Reference, a.Kind, a.Name, p)
}
if len(a.References) > 0 && !slices.Contains(a.References, a.Reference) {
return fmt.Errorf("delivery: the references of the %s %q do not name its reference %q", a.Kind, a.Name, a.Reference)
}
for _, r := range a.References {
if p := (BuiltArtifact{Name: a.Name, Kind: a.Kind, Digest: a.Digest, Reference: r}).referenceProblem(); p != "" {
return fmt.Errorf("delivery: the reference %q of the %s %q %s", r, a.Kind, a.Name, p)
}
}
}
for _, n := range b.Needs {
if !strings.HasPrefix(n, "form:") || len(n) == len("form:") {
return fmt.Errorf("delivery: the need %q of the build is not form:<name>", n)
}
}
if len(b.Needs) > 0 && b.State != BuildDone {
return fmt.Errorf("delivery: a %s build names needs; only a done key does", b.State)
}
for language, id := range b.Asks {
if b.State != BuildBuilding {
+24 -8
View File
@@ -26,11 +26,15 @@ func doneBuild() BuilderBuild {
func builderSamples() map[string]BuilderBuild {
failed := BuilderBuild{State: BuildFailed, By: "builder.novox.1", Step: "test", Error: "go test: 2 failed"}
both := doneBuild()
both.Needs = []string{"form:artifact-registry"}
both.Artifacts[1].References = []string{both.Artifacts[1].Reference, "artifact-store://mail/server@" + both.Artifacts[1].Digest}
return map[string]BuilderBuild{
"queued": {State: BuildQueued, Manifest: digestOf('4')},
"building": {State: BuildBuilding, By: "builder.novox.1", Step: "fetch", Manifest: digestOf('4')},
"done": doneBuild(),
"failed": failed,
"done with needs and both forms": both,
"queued": {State: BuildQueued, Manifest: digestOf('4')},
"building": {State: BuildBuilding, By: "builder.novox.1", Step: "fetch", Manifest: digestOf('4')},
"done": doneBuild(),
"failed": failed,
}
}
@@ -90,10 +94,17 @@ func TestBuilderBuildChecked(t *testing.T) {
edit func(*BuilderBuild)
words []string
}{
"done without a manifest": {func(b *BuilderBuild) { b.Manifest = "" }, []string{"manifest"}},
"a malformed manifest": {func(b *BuilderBuild) { b.Manifest = "abc" }, []string{"abc"}},
"an artifact without a digest": {func(b *BuilderBuild) { b.Artifacts[1].Digest = "" }, []string{"digest", "server"}},
"an artifact without a name": {func(b *BuilderBuild) { b.Artifacts[0].Name = "" }, []string{"name"}},
"done without a manifest": {func(b *BuilderBuild) { b.Manifest = "" }, []string{"manifest"}},
"a malformed manifest": {func(b *BuilderBuild) { b.Manifest = "abc" }, []string{"abc"}},
"an artifact without a digest": {func(b *BuilderBuild) { b.Artifacts[1].Digest = "" }, []string{"digest", "server"}},
"an artifact without a name": {func(b *BuilderBuild) { b.Artifacts[0].Name = "" }, []string{"name"}},
"a need that is not a form": {func(b *BuilderBuild) { b.Needs = []string{"word:x"} }, []string{"word:x", "form:"}},
"a reference not among the references": {func(b *BuilderBuild) {
b.Artifacts[1].References = []string{"artifact-store://mail/server@" + b.Artifacts[1].Digest}
}, []string{"server", "references"}},
"a reference of another artifact": {func(b *BuilderBuild) {
b.Artifacts[1].References = []string{b.Artifacts[1].Reference, "registry.example/mail/other@" + b.Artifacts[1].Digest}
}, []string{"server", "other"}},
"an unknown state": {func(b *BuilderBuild) { b.State = "paused" }, []string{"paused"}},
"a toolchain without a version": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Recipe: "3"} }, []string{"go", "version"}},
"a toolchain without a recipe": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Version: "1.26.0"} }, []string{"go", "recipe"}},
@@ -119,6 +130,11 @@ func TestBuilderBuildChecked(t *testing.T) {
}
// a queued or building value may not already carry an outcome
q := builderSamples()["queued"]
q.Needs = []string{"form:x"}
if _, err := EncodeBuilderBuild(q); err == nil {
t.Error("a queued build with needs accepted")
}
q.Needs = nil
q.Artifacts = doneBuild().Artifacts
if _, err := EncodeBuilderBuild(q); err == nil {
t.Error("a queued build with artifacts accepted")