From 37ff2ed1d1f87ba5b39a867e7bc960dacae47fea Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:16:16 +0100 Subject: [PATCH 1/3] mesh-delivery-contracts, sdk buildstep: the check job carries the toolchain the checker agreed (ADR 0584 clause 2) and the node names, so a node on another toolchain declines the check; the part had neither, the sdk's CheckJob lacked the toolchain (B2g; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- contracts/go/seats/nodebuildgo/contract.go | 10 +++++++--- contracts/go/seats/nodebuildgo/contract_test.go | 2 +- contracts/go/seats/nodebuildimage/contract.go | 10 +++++++--- contracts/go/seats/nodebuildnode/contract.go | 10 +++++++--- contracts/parts/build-step.json | 11 +++++++++++ contracts/seats/node-build-go/fixtures/states.json | 11 ++++++++++- .../ts/seats/nodebuildgo/src/parts/build-step.ts | 4 ++++ .../ts/seats/nodebuildimage/src/parts/build-step.ts | 4 ++++ .../ts/seats/nodebuildnode/src/parts/build-step.ts | 4 ++++ sdk/buildstep/ask.go | 6 ++++++ sdk/buildstep/ask_test.go | 13 +++++++++++++ 11 files changed, 74 insertions(+), 11 deletions(-) diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 3a31416a..7eec9ed5 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index 4aabcc73..abc309d5 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -39,7 +39,7 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.delivery$")}, - {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", + {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, {name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index e5445d8a..c9675571 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 9f2abc00..966a4add 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index 55920376..9ee1d5d0 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -130,6 +130,17 @@ "items": { "type": "string" } + }, + "toolchain": { + "$ref": "#/defs/ToolchainUsed", + "description": "the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check" + }, + "node-names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the node names the check's tests run against" } }, "required": [ diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 5193a6af..0898036e 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -91,7 +91,16 @@ "unit", "integration" ], - "kept": true + "kept": true, + "toolchain": { + "language": "go", + "version": "1.26.0", + "recipe": "2" + }, + "node-names": [ + "ace", + "g14" + ] } } }, diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/sdk/buildstep/ask.go b/sdk/buildstep/ask.go index be85a8af..1701cab3 100644 --- a/sdk/buildstep/ask.go +++ b/sdk/buildstep/ask.go @@ -258,6 +258,9 @@ type CheckJob struct { Run string `json:"run,omitempty"` Packages []string `json:"packages,omitempty"` NodeNames []string `json:"node-names,omitempty"` + // Toolchain is the toolchain the checker agreed for the language (ADR 0584 clause 2): a holder whose node reports + // another version declines the check. + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // ComposeJob asks one compose-candidate build. @@ -409,6 +412,9 @@ func (c CheckJob) check(id string) error { return fmt.Errorf("buildstep: the check ask %s names a package %q that is no import path", id, p) } } + if t := c.Toolchain; t != nil && (t.Language == "" || t.Version == "" || t.Recipe == "") { + return fmt.Errorf("buildstep: the check ask %s names a toolchain without its language, version and recipe", id) + } seen := map[string]bool{} for _, n := range c.NodeNames { if err := meshkeys.CheckName("node", n); err != nil || strings.Contains(n, ",") || seen[n] { diff --git a/sdk/buildstep/ask_test.go b/sdk/buildstep/ask_test.go index 07b2d67a..b452a1cb 100644 --- a/sdk/buildstep/ask_test.go +++ b/sdk/buildstep/ask_test.go @@ -72,6 +72,19 @@ func TestAskCheckAndRoundTrip(t *testing.T) { t.Errorf("a level: %v", err) } chk = goodCheck() + chk.Check.Toolchain = &ToolchainUsed{Language: "go", Version: "1.26.0", Recipe: "2"} + raw, err := EncodeStepAsk(chk) + if err != nil { + t.Fatal(err) + } + if back, err := DecodeStepAsk(raw); err != nil || back.Check.Toolchain == nil || back.Check.Toolchain.Version != "1.26.0" { + t.Errorf("the check's toolchain is lost in a round trip: %+v, %v", back.Check, err) + } + chk.Check.Toolchain.Recipe = "" + if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "toolchain") { + t.Errorf("a toolchain without a recipe: %v", err) + } + chk = goodCheck() chk.Language = AskImage if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "no tests") { t.Errorf("an image check: %v", err) From baa04d68cefd08f5f95e260311292dc2869d7464 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:17:33 +0100 Subject: [PATCH 2/3] mesh-delivery-contracts, sdk buildstep: the check answer's fetch keeps the fetch tool's whole answer (workspace, commit, modules, merge, conflicts, changed, programs, trees, beside, packages, installed, refused), typed in the seats' StepResult and the sdk's CheckAnswer and StepResult; fixtures from the build modules' captured answers (B2g; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- contracts/go/seats/nodebuildgo/contract.go | 70 +++++- .../go/seats/nodebuildgo/contract_test.go | 4 +- contracts/go/seats/nodebuildimage/contract.go | 70 +++++- contracts/go/seats/nodebuildnode/contract.go | 70 +++++- .../go/seats/nodebuildnode/contract_test.go | 2 + contracts/parts/build-step.json | 209 +++++++++++++++++- .../seats/node-build-go/fixtures/states.json | 49 +++- .../node-build-node/fixtures/states.json | 33 +++ contracts/ts/seats/nodebuildgo/src/index.ts | 6 + .../seats/nodebuildgo/src/parts/build-step.ts | 64 +++++- .../ts/seats/nodebuildimage/src/index.ts | 6 + .../nodebuildimage/src/parts/build-step.ts | 64 +++++- contracts/ts/seats/nodebuildnode/src/index.ts | 6 + .../nodebuildnode/src/parts/build-step.ts | 64 +++++- sdk/buildstep/ask.go | 73 +++++- sdk/buildstep/ask_test.go | 5 + 16 files changed, 768 insertions(+), 27 deletions(-) diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 7eec9ed5..d1a9288c 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -233,7 +233,7 @@ type Capacity struct { // CheckAnswer is a type of the contract. type CheckAnswer struct { - Fetch Fetch `json:"fetch"` + Fetch FetchAnswer `json:"fetch"` Levels []LevelAnswer `json:"levels,omitempty"` } @@ -271,6 +271,30 @@ type Fetch struct { Repository string `json:"repository"` } +// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package. +type FetchAnswer struct { + Base string `json:"base,omitempty"` + Beside []PlacedBeside `json:"beside,omitempty"` + Changed []string `json:"changed,omitempty"` + Commit string `json:"commit"` + Conflicts []string `json:"conflicts,omitempty"` + Group string `json:"group,omitempty"` + Installed []string `json:"installed,omitempty"` + Merge string `json:"merge,omitempty"` + Modules []string `json:"modules"` + Packages []TakenVersion `json:"packages,omitempty"` + Programs []Program `json:"programs,omitempty"` + Refused []FetchRefusal `json:"refused,omitempty"` + Trees map[string]ModuleTree `json:"trees,omitempty"` + Workspace string `json:"workspace"` +} + +// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why. +type FetchRefusal struct { + Module string `json:"module"` + Reason string `json:"reason"` +} + // LevelAnswerDigestNeedsItem is a type of the contract. type LevelAnswerDigestNeedsItem struct { Needs []string `json:"needs,omitempty"` @@ -316,6 +340,16 @@ type LevelAnswer struct { Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` } +// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths. +type ModuleTree struct { + Locks map[string]string `json:"locks,omitempty"` + Manifest string `json:"manifest"` + // no longer filled (ADR 0593 section 2); kept so a preview written before still reads + Packages []string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + Tree string `json:"tree"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -326,6 +360,21 @@ type Pause struct { V int64 `json:"v,omitempty"` } +// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory. +type PlacedBeside struct { + Commit string `json:"commit"` + Dir string `json:"dir"` + Repository string `json:"repository"` +} + +// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it. +type Program struct { + Commit string `json:"commit,omitempty"` + // ELF, Mach-O or PE + Kind string `json:"kind"` + Path string `json:"path"` +} + // Step is one step a build module runs on a node. type Step struct { // when (RFC 3339 time) @@ -409,10 +458,10 @@ type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` Built bool `json:"built,omitempty"` // the holder declined the ask: its node lacks what the ask needs - Declined bool `json:"declined,omitempty"` - Fetch *Fetch `json:"fetch,omitempty"` - Imports []string `json:"imports,omitempty"` - Key string `json:"key,omitempty"` + Declined bool `json:"declined,omitempty"` + Fetch *FetchAnswer `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` // what the holder's node lacks of it Lacks []string `json:"lacks,omitempty"` Levels []LevelAnswer `json:"levels,omitempty"` @@ -424,6 +473,17 @@ type StepResult struct { Verdict any `json:"verdict,omitempty"` } +// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said. +type TakenVersion struct { + Asked string `json:"asked"` + Built string `json:"built"` + Dir string `json:"dir"` + Named string `json:"named"` + Package string `json:"package"` + Said string `json:"said,omitempty"` + Sum string `json:"sum,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index abc309d5..9148353a 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -43,10 +43,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, {name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", + {name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a declined step", subject: "", given: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", wire: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finished check step whose head does not merge", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index c9675571..69fc44b7 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -233,7 +233,7 @@ type Capacity struct { // CheckAnswer is a type of the contract. type CheckAnswer struct { - Fetch Fetch `json:"fetch"` + Fetch FetchAnswer `json:"fetch"` Levels []LevelAnswer `json:"levels,omitempty"` } @@ -271,6 +271,30 @@ type Fetch struct { Repository string `json:"repository"` } +// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package. +type FetchAnswer struct { + Base string `json:"base,omitempty"` + Beside []PlacedBeside `json:"beside,omitempty"` + Changed []string `json:"changed,omitempty"` + Commit string `json:"commit"` + Conflicts []string `json:"conflicts,omitempty"` + Group string `json:"group,omitempty"` + Installed []string `json:"installed,omitempty"` + Merge string `json:"merge,omitempty"` + Modules []string `json:"modules"` + Packages []TakenVersion `json:"packages,omitempty"` + Programs []Program `json:"programs,omitempty"` + Refused []FetchRefusal `json:"refused,omitempty"` + Trees map[string]ModuleTree `json:"trees,omitempty"` + Workspace string `json:"workspace"` +} + +// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why. +type FetchRefusal struct { + Module string `json:"module"` + Reason string `json:"reason"` +} + // LevelAnswerDigestNeedsItem is a type of the contract. type LevelAnswerDigestNeedsItem struct { Needs []string `json:"needs,omitempty"` @@ -316,6 +340,16 @@ type LevelAnswer struct { Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` } +// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths. +type ModuleTree struct { + Locks map[string]string `json:"locks,omitempty"` + Manifest string `json:"manifest"` + // no longer filled (ADR 0593 section 2); kept so a preview written before still reads + Packages []string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + Tree string `json:"tree"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -326,6 +360,21 @@ type Pause struct { V int64 `json:"v,omitempty"` } +// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory. +type PlacedBeside struct { + Commit string `json:"commit"` + Dir string `json:"dir"` + Repository string `json:"repository"` +} + +// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it. +type Program struct { + Commit string `json:"commit,omitempty"` + // ELF, Mach-O or PE + Kind string `json:"kind"` + Path string `json:"path"` +} + // Step is one step a build module runs on a node. type Step struct { // when (RFC 3339 time) @@ -409,10 +458,10 @@ type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` Built bool `json:"built,omitempty"` // the holder declined the ask: its node lacks what the ask needs - Declined bool `json:"declined,omitempty"` - Fetch *Fetch `json:"fetch,omitempty"` - Imports []string `json:"imports,omitempty"` - Key string `json:"key,omitempty"` + Declined bool `json:"declined,omitempty"` + Fetch *FetchAnswer `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` // what the holder's node lacks of it Lacks []string `json:"lacks,omitempty"` Levels []LevelAnswer `json:"levels,omitempty"` @@ -424,6 +473,17 @@ type StepResult struct { Verdict any `json:"verdict,omitempty"` } +// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said. +type TakenVersion struct { + Asked string `json:"asked"` + Built string `json:"built"` + Dir string `json:"dir"` + Named string `json:"named"` + Package string `json:"package"` + Said string `json:"said,omitempty"` + Sum string `json:"sum,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 966a4add..51a5a76e 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -233,7 +233,7 @@ type Capacity struct { // CheckAnswer is a type of the contract. type CheckAnswer struct { - Fetch Fetch `json:"fetch"` + Fetch FetchAnswer `json:"fetch"` Levels []LevelAnswer `json:"levels,omitempty"` } @@ -271,6 +271,30 @@ type Fetch struct { Repository string `json:"repository"` } +// FetchAnswer is what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package. +type FetchAnswer struct { + Base string `json:"base,omitempty"` + Beside []PlacedBeside `json:"beside,omitempty"` + Changed []string `json:"changed,omitempty"` + Commit string `json:"commit"` + Conflicts []string `json:"conflicts,omitempty"` + Group string `json:"group,omitempty"` + Installed []string `json:"installed,omitempty"` + Merge string `json:"merge,omitempty"` + Modules []string `json:"modules"` + Packages []TakenVersion `json:"packages,omitempty"` + Programs []Program `json:"programs,omitempty"` + Refused []FetchRefusal `json:"refused,omitempty"` + Trees map[string]ModuleTree `json:"trees,omitempty"` + Workspace string `json:"workspace"` +} + +// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why. +type FetchRefusal struct { + Module string `json:"module"` + Reason string `json:"reason"` +} + // LevelAnswerDigestNeedsItem is a type of the contract. type LevelAnswerDigestNeedsItem struct { Needs []string `json:"needs,omitempty"` @@ -316,6 +340,16 @@ type LevelAnswer struct { Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` } +// ModuleTree is the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths. +type ModuleTree struct { + Locks map[string]string `json:"locks,omitempty"` + Manifest string `json:"manifest"` + // no longer filled (ADR 0593 section 2); kept so a preview written before still reads + Packages []string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + Tree string `json:"tree"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -326,6 +360,21 @@ type Pause struct { V int64 `json:"v,omitempty"` } +// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory. +type PlacedBeside struct { + Commit string `json:"commit"` + Dir string `json:"dir"` + Repository string `json:"repository"` +} + +// Program is a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it. +type Program struct { + Commit string `json:"commit,omitempty"` + // ELF, Mach-O or PE + Kind string `json:"kind"` + Path string `json:"path"` +} + // Step is one step a build module runs on a node. type Step struct { // when (RFC 3339 time) @@ -409,10 +458,10 @@ type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` Built bool `json:"built,omitempty"` // the holder declined the ask: its node lacks what the ask needs - Declined bool `json:"declined,omitempty"` - Fetch *Fetch `json:"fetch,omitempty"` - Imports []string `json:"imports,omitempty"` - Key string `json:"key,omitempty"` + Declined bool `json:"declined,omitempty"` + Fetch *FetchAnswer `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` // what the holder's node lacks of it Lacks []string `json:"lacks,omitempty"` Levels []LevelAnswer `json:"levels,omitempty"` @@ -424,6 +473,17 @@ type StepResult struct { Verdict any `json:"verdict,omitempty"` } +// TakenVersion is of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said. +type TakenVersion struct { + Asked string `json:"asked"` + Built string `json:"built"` + Dir string `json:"dir"` + Named string `json:"named"` + Package string `json:"package"` + Said string `json:"said,omitempty"` + Sum string `json:"sum,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go index 9bfd4c18..70e0c3a9 100644 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -39,6 +39,8 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-node.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.delivery$")}, + {name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index 9ee1d5d0..c272a8c2 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -229,7 +229,7 @@ "type": "object", "properties": { "fetch": { - "$ref": "#/defs/Fetch" + "$ref": "#/defs/FetchAnswer" }, "levels": { "type": "array", @@ -691,7 +691,7 @@ } }, "fetch": { - "$ref": "#/defs/Fetch" + "$ref": "#/defs/FetchAnswer" }, "levels": { "type": "array", @@ -727,6 +727,211 @@ "description": "what the holder's node lacks of it" } } + }, + "ModuleTree": { + "type": "object", + "description": "the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths", + "properties": { + "tree": { + "type": "string" + }, + "manifest": { + "type": "string" + }, + "locks": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "paths": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "packages": { + "type": "array", + "items": { + "type": "string" + }, + "description": "no longer filled (ADR 0593 section 2); kept so a preview written before still reads" + } + }, + "required": [ + "tree", + "manifest" + ] + }, + "Program": { + "type": "object", + "description": "a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it", + "properties": { + "path": { + "type": "string" + }, + "kind": { + "type": "string", + "description": "ELF, Mach-O or PE" + }, + "commit": { + "type": "string" + } + }, + "required": [ + "path", + "kind" + ] + }, + "PlacedBeside": { + "type": "object", + "description": "a sibling repository placed as a plain directory under the workspace's beside directory", + "properties": { + "repository": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "dir": { + "type": "string" + } + }, + "required": [ + "repository", + "commit", + "dir" + ] + }, + "TakenVersion": { + "type": "object", + "description": "of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said", + "properties": { + "dir": { + "type": "string" + }, + "package": { + "type": "string" + }, + "asked": { + "type": "string" + }, + "named": { + "type": "string" + }, + "built": { + "type": "string" + }, + "sum": { + "type": "string" + }, + "said": { + "type": "string" + } + }, + "required": [ + "dir", + "package", + "asked", + "named", + "built" + ] + }, + "FetchRefusal": { + "type": "object", + "description": "a TypeScript module whose packages the fetch did not install, with why", + "properties": { + "module": { + "type": "string" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "module", + "reason" + ] + }, + "FetchAnswer": { + "type": "object", + "description": "what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package", + "properties": { + "workspace": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + } + }, + "base": { + "type": "string" + }, + "merge": { + "type": "string" + }, + "conflicts": { + "type": "array", + "items": { + "type": "string" + } + }, + "changed": { + "type": "array", + "items": { + "type": "string" + } + }, + "programs": { + "type": "array", + "items": { + "$ref": "#/defs/Program" + } + }, + "trees": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/ModuleTree" + } + }, + "beside": { + "type": "array", + "items": { + "$ref": "#/defs/PlacedBeside" + } + }, + "packages": { + "type": "array", + "items": { + "$ref": "#/defs/TakenVersion" + } + }, + "installed": { + "type": "array", + "items": { + "type": "string" + } + }, + "refused": { + "type": "array", + "items": { + "$ref": "#/defs/FetchRefusal" + } + }, + "group": { + "type": "string" + } + }, + "required": [ + "workspace", + "commit", + "modules" + ] } } } diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 0898036e..1a231fd5 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -147,8 +147,26 @@ "ended": "2026-11-05T10:06:00Z", "result": { "fetch": { - "repository": "novox/mesh-catalog", - "commit": "0123456789abcdef0123456789abcdef01234567" + "workspace": "mvii1iwu5414e4", + "commit": "0123456789abcdef0123456789abcdef01234567", + "modules": [ + "." + ], + "base": "abcdef0123456789abcdef0123456789abcdef01", + "merge": "fedcba9876543210fedcba9876543210fedcba98", + "changed": [ + "a.go", + "modules/m/main.go" + ], + "trees": { + "modules/m": { + "tree": "1111111111111111111111111111111111111111", + "manifest": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356", + "locks": { + "go.sum": "c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9" + } + } + } }, "levels": [ { @@ -208,5 +226,32 @@ ] } } + }, + { + "name": "a finished check step whose head does not merge", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "c3d5", + "tool": "check", + "node": "ace", + "state": "passed", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:01:00Z", + "result": { + "fetch": { + "workspace": "mvii1iwz5050eb", + "commit": "0123456789abcdef0123456789abcdef01234567", + "modules": [ + "." + ], + "base": "abcdef0123456789abcdef0123456789abcdef01", + "conflicts": [ + "a.go", + "sub/b.go" + ] + } + } + } } ] diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json index 9a80337a..a9f265f5 100644 --- a/contracts/seats/node-build-node/fixtures/states.json +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -59,5 +59,38 @@ ] } } + }, + { + "name": "a finished check step of a TypeScript merge preview", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "c3d6", + "tool": "check", + "node": "ace", + "state": "passed", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:01:00Z", + "result": { + "fetch": { + "workspace": "mvii1sxt979f67", + "commit": "0123456789abcdef0123456789abcdef01234567", + "modules": [ + "modules/m" + ], + "base": "abcdef0123456789abcdef0123456789abcdef01", + "merge": "0123456789abcdef0123456789abcdef01234567", + "changed": [ + "modules/m/package.json" + ], + "trees": { + "modules/m": { + "tree": "0123456789abcdef0123456789abcdef01234567", + "manifest": "0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef" + } + } + } + } + } } ] diff --git a/contracts/ts/seats/nodebuildgo/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts index 48102965..83d2a60a 100644 --- a/contracts/ts/seats/nodebuildgo/src/index.ts +++ b/contracts/ts/seats/nodebuildgo/src/index.ts @@ -111,16 +111,21 @@ export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; +export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; +export type ModuleTree = buildStep.ModuleTree; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; +export type PlacedBeside = buildStep.PlacedBeside; +export type Program = buildStep.Program; export type ResumeAnswer = buildStep.ResumeAnswer; export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; @@ -129,6 +134,7 @@ export type StepAskBuild = buildStep.StepAskBuild; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; export type StepResult = buildStep.StepResult; +export type TakenVersion = buildStep.TakenVersion; export type Toolchain = buildStep.Toolchain; export type ToolchainUsed = buildStep.ToolchainUsed; diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index 2ce76a13..c61ca3bc 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -51,7 +51,7 @@ export interface Capacity { } export interface CheckAnswer { - "fetch": Fetch; + "fetch": FetchAnswer; "levels"?: LevelAnswer[]; } @@ -87,6 +87,30 @@ export interface Fetch { "repository": string; } +/** what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package */ +export interface FetchAnswer { + "base"?: string; + "beside"?: PlacedBeside[]; + "changed"?: string[]; + "commit": string; + "conflicts"?: string[]; + "group"?: string; + "installed"?: string[]; + "merge"?: string; + "modules": string[]; + "packages"?: TakenVersion[]; + "programs"?: Program[]; + "refused"?: FetchRefusal[]; + "trees"?: Record; + "workspace": string; +} + +/** a TypeScript module whose packages the fetch did not install, with why */ +export interface FetchRefusal { + "module": string; + "reason": string; +} + export interface LevelAnswerDigestNeedsItem { "needs"?: string[]; "package"?: string; @@ -127,6 +151,16 @@ export interface LevelAnswer { "unmet"?: LevelAnswerUnmetItem[]; } +/** the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths */ +export interface ModuleTree { + "locks"?: Record; + "manifest": string; + /** no longer filled (ADR 0593 section 2); kept so a preview written before still reads */ + "packages"?: string[]; + "paths"?: Record; + "tree": string; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -137,6 +171,21 @@ export interface Pause { "v"?: number; } +/** a sibling repository placed as a plain directory under the workspace's beside directory */ +export interface PlacedBeside { + "commit": string; + "dir": string; + "repository": string; +} + +/** a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it */ +export interface Program { + "commit"?: string; + /** ELF, Mach-O or PE */ + "kind": string; + "path": string; +} + /** one step a build module runs on a node */ export interface Step { /** when (RFC 3339 time) */ @@ -218,7 +267,7 @@ export interface StepResult { "built"?: boolean; /** the holder declined the ask: its node lacks what the ask needs */ "declined"?: boolean; - "fetch"?: Fetch; + "fetch"?: FetchAnswer; "imports"?: string[]; "key"?: string; /** what the holder's node lacks of it */ @@ -232,6 +281,17 @@ export interface StepResult { "verdict"?: unknown; } +/** of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said */ +export interface TakenVersion { + "asked": string; + "built": string; + "dir": string; + "named": string; + "package": string; + "said"?: string; + "sum"?: string; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ diff --git a/contracts/ts/seats/nodebuildimage/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts index f0312a18..d0745fcc 100644 --- a/contracts/ts/seats/nodebuildimage/src/index.ts +++ b/contracts/ts/seats/nodebuildimage/src/index.ts @@ -111,16 +111,21 @@ export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; +export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; +export type ModuleTree = buildStep.ModuleTree; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; +export type PlacedBeside = buildStep.PlacedBeside; +export type Program = buildStep.Program; export type ResumeAnswer = buildStep.ResumeAnswer; export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; @@ -129,6 +134,7 @@ export type StepAskBuild = buildStep.StepAskBuild; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; export type StepResult = buildStep.StepResult; +export type TakenVersion = buildStep.TakenVersion; export type Toolchain = buildStep.Toolchain; export type ToolchainUsed = buildStep.ToolchainUsed; diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index 2ce76a13..c61ca3bc 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -51,7 +51,7 @@ export interface Capacity { } export interface CheckAnswer { - "fetch": Fetch; + "fetch": FetchAnswer; "levels"?: LevelAnswer[]; } @@ -87,6 +87,30 @@ export interface Fetch { "repository": string; } +/** what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package */ +export interface FetchAnswer { + "base"?: string; + "beside"?: PlacedBeside[]; + "changed"?: string[]; + "commit": string; + "conflicts"?: string[]; + "group"?: string; + "installed"?: string[]; + "merge"?: string; + "modules": string[]; + "packages"?: TakenVersion[]; + "programs"?: Program[]; + "refused"?: FetchRefusal[]; + "trees"?: Record; + "workspace": string; +} + +/** a TypeScript module whose packages the fetch did not install, with why */ +export interface FetchRefusal { + "module": string; + "reason": string; +} + export interface LevelAnswerDigestNeedsItem { "needs"?: string[]; "package"?: string; @@ -127,6 +151,16 @@ export interface LevelAnswer { "unmet"?: LevelAnswerUnmetItem[]; } +/** the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths */ +export interface ModuleTree { + "locks"?: Record; + "manifest": string; + /** no longer filled (ADR 0593 section 2); kept so a preview written before still reads */ + "packages"?: string[]; + "paths"?: Record; + "tree": string; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -137,6 +171,21 @@ export interface Pause { "v"?: number; } +/** a sibling repository placed as a plain directory under the workspace's beside directory */ +export interface PlacedBeside { + "commit": string; + "dir": string; + "repository": string; +} + +/** a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it */ +export interface Program { + "commit"?: string; + /** ELF, Mach-O or PE */ + "kind": string; + "path": string; +} + /** one step a build module runs on a node */ export interface Step { /** when (RFC 3339 time) */ @@ -218,7 +267,7 @@ export interface StepResult { "built"?: boolean; /** the holder declined the ask: its node lacks what the ask needs */ "declined"?: boolean; - "fetch"?: Fetch; + "fetch"?: FetchAnswer; "imports"?: string[]; "key"?: string; /** what the holder's node lacks of it */ @@ -232,6 +281,17 @@ export interface StepResult { "verdict"?: unknown; } +/** of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said */ +export interface TakenVersion { + "asked": string; + "built": string; + "dir": string; + "named": string; + "package": string; + "said"?: string; + "sum"?: string; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ diff --git a/contracts/ts/seats/nodebuildnode/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts index 99f5963c..0c66706f 100644 --- a/contracts/ts/seats/nodebuildnode/src/index.ts +++ b/contracts/ts/seats/nodebuildnode/src/index.ts @@ -111,16 +111,21 @@ export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; +export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; +export type ModuleTree = buildStep.ModuleTree; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; +export type PlacedBeside = buildStep.PlacedBeside; +export type Program = buildStep.Program; export type ResumeAnswer = buildStep.ResumeAnswer; export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; @@ -129,6 +134,7 @@ export type StepAskBuild = buildStep.StepAskBuild; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; export type StepResult = buildStep.StepResult; +export type TakenVersion = buildStep.TakenVersion; export type Toolchain = buildStep.Toolchain; export type ToolchainUsed = buildStep.ToolchainUsed; diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index 2ce76a13..c61ca3bc 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -51,7 +51,7 @@ export interface Capacity { } export interface CheckAnswer { - "fetch": Fetch; + "fetch": FetchAnswer; "levels"?: LevelAnswer[]; } @@ -87,6 +87,30 @@ export interface Fetch { "repository": string; } +/** what a build module's fetch tool answers, whole: the workspace it made, the commit in it, the module directories found; and, when the call gave a base, the merge preview (the trunk commit, the merge commit, the files that conflict, the paths the merge changes, the compiled programs among them, the tree of each module directory). A head that does not merge cleanly is an answer, not an error: merge is empty and conflicts names the files. The Go module adds the sibling repositories placed and the package versions taken; the TypeScript module the directories installed and refused and the group's package */ +export interface FetchAnswer { + "base"?: string; + "beside"?: PlacedBeside[]; + "changed"?: string[]; + "commit": string; + "conflicts"?: string[]; + "group"?: string; + "installed"?: string[]; + "merge"?: string; + "modules": string[]; + "packages"?: TakenVersion[]; + "programs"?: Program[]; + "refused"?: FetchRefusal[]; + "trees"?: Record; + "workspace": string; +} + +/** a TypeScript module whose packages the fetch did not install, with why */ +export interface FetchRefusal { + "module": string; + "reason": string; +} + export interface LevelAnswerDigestNeedsItem { "needs"?: string[]; "package"?: string; @@ -127,6 +151,16 @@ export interface LevelAnswer { "unmet"?: LevelAnswerUnmetItem[]; } +/** the tree of one module directory in a merge preview: its git tree, the digest of its manifest, the lock files' digests and the digests of the named paths */ +export interface ModuleTree { + "locks"?: Record; + "manifest": string; + /** no longer filled (ADR 0593 section 2); kept so a preview written before still reads */ + "packages"?: string[]; + "paths"?: Record; + "tree": string; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -137,6 +171,21 @@ export interface Pause { "v"?: number; } +/** a sibling repository placed as a plain directory under the workspace's beside directory */ +export interface PlacedBeside { + "commit": string; + "dir": string; + "repository": string; +} + +/** a changed file that is a compiled program (issue 1577): its path, its executable format, and the commit of the branch that added it when the merge's tree no longer holds it */ +export interface Program { + "commit"?: string; + /** ELF, Mach-O or PE */ + "kind": string; + "path": string; +} + /** one step a build module runs on a node */ export interface Step { /** when (RFC 3339 time) */ @@ -218,7 +267,7 @@ export interface StepResult { "built"?: boolean; /** the holder declined the ask: its node lacks what the ask needs */ "declined"?: boolean; - "fetch"?: Fetch; + "fetch"?: FetchAnswer; "imports"?: string[]; "key"?: string; /** what the holder's node lacks of it */ @@ -232,6 +281,17 @@ export interface StepResult { "verdict"?: unknown; } +/** of a directory whose go.mod requires an asked mesh package, the version it is built against: the asked one, or go.mod's when it names a newer one, said */ +export interface TakenVersion { + "asked": string; + "built": string; + "dir": string; + "named": string; + "package": string; + "said"?: string; + "sum"?: string; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ diff --git a/sdk/buildstep/ask.go b/sdk/buildstep/ask.go index 1701cab3..7e46704e 100644 --- a/sdk/buildstep/ask.go +++ b/sdk/buildstep/ask.go @@ -526,10 +526,81 @@ type AskBuilt struct { // CheckAnswer is what a check ask's step answers. type CheckAnswer struct { - Fetch Fetch `json:"fetch"` + Fetch FetchAnswer `json:"fetch"` Levels []LevelAnswer `json:"levels,omitempty"` } +// FetchAnswer is what a build module's fetch tool answers, whole: the part's `FetchAnswer`. A check keeps it as the +// answer's fetch, so the checker reads a conflict (a head that does not merge is an answer, not an error), the changed +// paths, the programs and the trees from it. +type FetchAnswer struct { + Workspace string `json:"workspace"` + Commit string `json:"commit"` + Modules []string `json:"modules"` + // The merge preview, only when the call gave a base. + Base string `json:"base,omitempty"` + Merge string `json:"merge,omitempty"` + Conflicts []string `json:"conflicts,omitempty"` + Changed []string `json:"changed,omitempty"` + Programs []Program `json:"programs,omitempty"` + Trees map[string]ModuleTree `json:"trees,omitempty"` + // The Go module's. + Beside []PlacedBeside `json:"beside,omitempty"` + Packages []TakenVersion `json:"packages,omitempty"` + // The TypeScript module's. + Installed []string `json:"installed,omitempty"` + Refused []FetchRefusal `json:"refused,omitempty"` + Group string `json:"group,omitempty"` +} + +// Program is a changed file that is a compiled program (issue 1577). +type Program struct { + Path string `json:"path"` + Kind string `json:"kind"` // ELF, Mach-O or PE + Commit string `json:"commit,omitempty"` +} + +// PlacedBeside is a sibling repository placed as a plain directory under the workspace's beside directory. +type PlacedBeside struct { + Repository string `json:"repository"` + Commit string `json:"commit"` + Dir string `json:"dir"` +} + +// TakenVersion is, of a directory whose go.mod requires an asked mesh package, the version it is built against. +type TakenVersion struct { + Dir string `json:"dir"` + Package string `json:"package"` + Asked string `json:"asked"` + Named string `json:"named"` + Built string `json:"built"` + Sum string `json:"sum,omitempty"` + Said string `json:"said,omitempty"` +} + +// FetchRefusal is a TypeScript module whose packages the fetch did not install, with why. +type FetchRefusal struct { + Module string `json:"module"` + Reason string `json:"reason"` +} + +// StepResult is what any step answered: one layout for the four kinds, as the seats' `StepResult` (the fields of the +// kinds do not meet). A step's result bytes read into it strictly. +type StepResult struct { + Artifacts []AskBuilt `json:"artifacts,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` + Imports []string `json:"imports,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Fetch *FetchAnswer `json:"fetch,omitempty"` + Levels []LevelAnswer `json:"levels,omitempty"` + Built bool `json:"built,omitempty"` + Key string `json:"key,omitempty"` + Verdict json.RawMessage `json:"verdict,omitempty"` + Declined bool `json:"declined,omitempty"` + Needs []string `json:"needs,omitempty"` + Lacks []string `json:"lacks,omitempty"` +} + // ComposeAnswer is what a compose ask's step answers. type ComposeAnswer struct { Built bool `json:"built"` diff --git a/sdk/buildstep/ask_test.go b/sdk/buildstep/ask_test.go index b452a1cb..e75ef4a6 100644 --- a/sdk/buildstep/ask_test.go +++ b/sdk/buildstep/ask_test.go @@ -264,6 +264,11 @@ func TestTheThreeSeatsFixturesFitTheNeutralForms(t *testing.T) { case strings.HasPrefix(f.Of, "state:steps/"): seen++ strict(t, f.Given, &Step{}) + var st Step + if err := json.Unmarshal(f.Given, &st); err == nil && len(st.Result) > 0 { + // a step's result keeps what the tool answered, whole (the fetch tool's answer too) + strict(t, st.Result, &StepResult{}) + } } }) } From 6d46b8a59e6c99c7ca6b4cb0e8d399d473966b22 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:18:04 +0100 Subject: [PATCH 3/3] mesh-delivery-sdk buildstep: Toolchain, ToolchainKey and the codec of a node's toolchains value, shared by the three build modules, move here from mesh-build-go's own package; a test reads each seat's fixture strictly into it (B2g; ADR 0634 section 6; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- sdk/buildstep/toolchain.go | 81 +++++++++++++++++++++++++++++++++ sdk/buildstep/toolchain_test.go | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 sdk/buildstep/toolchain.go create mode 100644 sdk/buildstep/toolchain_test.go diff --git a/sdk/buildstep/toolchain.go b/sdk/buildstep/toolchain.go new file mode 100644 index 00000000..9b655926 --- /dev/null +++ b/sdk/buildstep/toolchain.go @@ -0,0 +1,81 @@ +package buildstep + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "time" +) + +// ToolchainVersion is the layout version of a toolchains value. +const ToolchainVersion = 1 + +// Toolchain is the value of a build module's state toolchains under the key .: the version of the +// language's toolchain installed on the node, the build module's recipe version, and when the module saw them. It is +// the part build-step's `Toolchain`, the layout every node-build seat gives that state; the three build modules share +// this neutral form (a seat's generated type is one package per seat). A test reads each seat's fixture strictly into it. +type Toolchain struct { + V int `json:"v,omitempty"` + Language string `json:"language"` + Version string `json:"version"` + Recipe string `json:"recipe"` + At time.Time `json:"at,omitzero"` +} + +func checkToolchainLanguage(language string) error { + switch language { + case "go", "typescript": + return nil + } + return fmt.Errorf("buildstep: the toolchain language %q is not go or typescript", language) +} + +// ToolchainKey is the key of a node's toolchain of one language: .. It refuses an empty node, a node +// with a dot and a language other than go or typescript. +func ToolchainKey(node, language string) (string, error) { + if node == "" || strings.ContainsAny(node, ". ") { + return "", fmt.Errorf("buildstep: %q is not a node name", node) + } + if err := checkToolchainLanguage(language); err != nil { + return "", err + } + return node + "." + language, nil +} + +func (t Toolchain) check() error { + if err := checkToolchainLanguage(t.Language); err != nil { + return err + } + if t.Version == "" || t.Recipe == "" { + return fmt.Errorf("buildstep: a %s toolchain needs its version and its recipe", t.Language) + } + return nil +} + +// EncodeToolchain is t as JSON; it refuses a language other than go or typescript and an empty version or recipe. +func EncodeToolchain(t Toolchain) ([]byte, error) { + if err := t.check(); err != nil { + return nil, err + } + t.V = ToolchainVersion + return json.Marshal(t) +} + +// DecodeToolchain reads a toolchains value, refusing an unknown field, another layout version and what +// EncodeToolchain refuses. +func DecodeToolchain(data []byte) (Toolchain, error) { + var t Toolchain + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&t); err != nil { + return Toolchain{}, fmt.Errorf("buildstep: a toolchain does not read: %w", err) + } + if t.V != ToolchainVersion { + return Toolchain{}, fmt.Errorf("buildstep: a toolchain of version %d, this reads %d", t.V, ToolchainVersion) + } + if err := t.check(); err != nil { + return Toolchain{}, err + } + return t, nil +} diff --git a/sdk/buildstep/toolchain_test.go b/sdk/buildstep/toolchain_test.go new file mode 100644 index 00000000..3cff6eea --- /dev/null +++ b/sdk/buildstep/toolchain_test.go @@ -0,0 +1,80 @@ +package buildstep + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// The toolchains value and key are the layout all three node-build seats generate: each seat's fixture of the state +// reads strictly into Toolchain, and the key is the seat's key form .. +func TestTheToolchainsValueAndKeyAreTheSeatsLayout(t *testing.T) { + read := 0 + for _, seat := range []string{"node-build-go", "node-build-image", "node-build-node"} { + raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "seats", seat, "fixtures", "states.json")) + if err != nil { + t.Fatal(err) + } + var fixtures []struct { + Of string `json:"of"` + Given json.RawMessage `json:"given"` + } + if err := json.Unmarshal(raw, &fixtures); err != nil { + t.Fatal(err) + } + for _, f := range fixtures { + if f.Of != "state:toolchains/." { + continue + } + if back, err := DecodeToolchain(f.Given); err != nil || back.Version == "" { + t.Errorf("%s: the fixture reads as %+v, %v", seat, back, err) + } + if _, err := EncodeToolchain(mustToolchain(t, f.Given)); err != nil { + t.Errorf("%s: %v", seat, err) + } + dec := json.NewDecoder(bytes.NewReader(f.Given)) + dec.DisallowUnknownFields() + if err := dec.Decode(&Toolchain{}); err != nil { + t.Errorf("%s: %v", seat, err) + } + read++ + } + } + if read != 3 { + t.Errorf("%d toolchain fixtures read, want 3", read) + } + raw, err := EncodeToolchain(Toolchain{Language: "go", Version: "go1.27.1", Recipe: "3", At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)}) + if err != nil { + t.Fatal(err) + } + if back, err := DecodeToolchain(raw); err != nil || back.Version != "go1.27.1" || back.At.IsZero() { + t.Errorf("the value reads back as %+v, %v", back, err) + } + if key, err := ToolchainKey("ace", "typescript"); err != nil || key != "ace.typescript" { + t.Errorf("key %q, %v", key, err) + } + for _, bad := range [][2]string{{"", "go"}, {"a.b", "go"}, {"ace", "rust"}} { + if _, err := ToolchainKey(bad[0], bad[1]); err == nil { + t.Errorf("the key of %v was made", bad) + } + } + if _, err := EncodeToolchain(Toolchain{Language: "go", Version: "go1.27"}); err == nil { + t.Error("a toolchain without its recipe was written") + } + if _, err := DecodeToolchain([]byte(`{"v":1,"language":"go","version":"x","recipe":"1","extra":1}`)); err == nil || !strings.Contains(err.Error(), "extra") { + t.Errorf("a field the layout lacks was read: %v", err) + } +} + +func mustToolchain(t *testing.T, raw []byte) Toolchain { + t.Helper() + var tc Toolchain + if err := json.Unmarshal(raw, &tc); err != nil { + t.Fatal(err) + } + return tc +}