From baa04d68cefd08f5f95e260311292dc2869d7464 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:17:33 +0100 Subject: [PATCH] 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{}) + } } }) }