From 940d42c659837062ebd973c99e264784bd5c1c0c Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:20:54 +0100 Subject: [PATCH 1/3] CT5: the build seats' steps takes drop and why, fetch gains builds, a build names what it resolved, steps are keyed by asker, toolchain-reported and step-progress; regenerated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit steps callers: the runner, the indexer, the operator and sessions (D-18); BLD12 builds in the fetch; BLD13 resolved and the toolchain digest; BLD14 the step key .. beside main's .; the events toolchain-reported and step-progress on the three build seats, and step-progress on mesh-wants. Builds: design 47 G §463, §480, §465, §466, §467, §481, §483, §492, §496 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- contracts/go/seats/meshwants/contract.go | 30 ++- contracts/go/seats/meshwants/contract_test.go | 2 + contracts/go/seats/nodebuildgo/contract.go | 146 ++++++++++++++- .../go/seats/nodebuildgo/contract_test.go | 16 ++ contracts/go/seats/nodebuildimage/contract.go | 146 ++++++++++++++- .../go/seats/nodebuildimage/contract_test.go | 16 ++ contracts/go/seats/nodebuildnode/contract.go | 146 ++++++++++++++- .../go/seats/nodebuildnode/contract_test.go | 16 ++ contracts/parts/build-step.json | 174 +++++++++++++++++- .../seats/mesh-wants/fixtures/events.json | 11 ++ contracts/seats/mesh-wants/seat.json | 39 ++++ .../seats/node-build-go/fixtures/events.json | 23 +++ .../seats/node-build-go/fixtures/states.json | 12 ++ .../seats/node-build-go/fixtures/verbs.json | 24 +++ contracts/seats/node-build-go/seat.json | 9 +- .../node-build-image/fixtures/events.json | 23 +++ .../node-build-image/fixtures/states.json | 12 ++ .../node-build-image/fixtures/verbs.json | 24 +++ contracts/seats/node-build-image/seat.json | 9 +- .../node-build-node/fixtures/events.json | 23 +++ .../node-build-node/fixtures/states.json | 12 ++ .../seats/node-build-node/fixtures/verbs.json | 24 +++ contracts/seats/node-build-node/seat.json | 9 +- contracts/ts/seats/meshwants/index.d.ts | 15 ++ contracts/ts/seats/meshwants/index.js | 9 + contracts/ts/seats/nodebuildgo/index.d.ts | 29 +++ contracts/ts/seats/nodebuildgo/index.js | 41 +++++ .../seats/nodebuildgo/parts/build-step.d.ts | 64 ++++++- .../ts/seats/nodebuildgo/parts/build-step.js | 2 + contracts/ts/seats/nodebuildimage/index.d.ts | 29 +++ contracts/ts/seats/nodebuildimage/index.js | 41 +++++ .../nodebuildimage/parts/build-step.d.ts | 64 ++++++- .../seats/nodebuildimage/parts/build-step.js | 2 + contracts/ts/seats/nodebuildnode/index.d.ts | 29 +++ contracts/ts/seats/nodebuildnode/index.js | 41 +++++ .../seats/nodebuildnode/parts/build-step.d.ts | 64 ++++++- .../seats/nodebuildnode/parts/build-step.js | 2 + 37 files changed, 1331 insertions(+), 47 deletions(-) diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go index 97003e33..9b9f4754 100644 --- a/contracts/go/seats/meshwants/contract.go +++ b/contracts/go/seats/meshwants/contract.go @@ -39,6 +39,7 @@ const ( EventRoom = "room" EventStepEnded = "step-ended" EventVersionDelivered = "version-delivered" + EventStepProgress = "step-progress" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -48,7 +49,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPolicyChanged, EventAssignmentRevision, EventDifference, EventWanted, EventWaitHeld, EventWaitEnded, EventFilling, EventFull, EventRoom, EventStepEnded, EventVersionDelivered} + return []string{EventPolicyChanged, EventAssignmentRevision, EventDifference, EventWanted, EventWaitHeld, EventWaitEnded, EventFilling, EventFull, EventRoom, EventStepEnded, EventVersionDelivered, EventStepProgress} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -99,6 +100,17 @@ const StreamVersionDelivered = "SEAT_mesh-wants_version-delivered" // SubjectVersionDelivered is where version-delivered is published. const SubjectVersionDelivered = "mesh.seat.mesh-wants.event.version-delivered" +// StreamStepProgress is the stream step-progress is kept in. +const StreamStepProgress = "SEAT_mesh-wants_step-progress" + +// SubjectStepProgress is where step-progress is published, on mesh.seat.mesh-wants.event.step-progress.. +func SubjectStepProgress(node string) string { + return "mesh.seat.mesh-wants.event.step-progress." + node +} + +// SubjectStepProgressAll hears every step-progress. +const SubjectStepProgressAll = "mesh.seat.mesh-wants.event.step-progress.*" + // AcceptSubjectStep is where step is sent. const AcceptSubjectStep = "mesh.seat.mesh-wants.accept.step" @@ -823,6 +835,22 @@ type VersionDeliveredEvent struct { Version string `json:"version"` } +// StepProgressEvent is the body of the event step-progress. +type StepProgressEvent struct { + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + Phase string `json:"phase"` +} + +// The values of StepProgressEvent's phase. +const ( + StepProgressEventPhaseFetch = "fetch" + StepProgressEventPhaseBuild = "build" + StepProgressEventPhaseRun = "run" + StepProgressEventPhaseReport = "report" +) + // WantsByWantNodeModuleValue is one value of the state wants under the key want... type WantsByWantNodeModuleValue struct { // again or skip diff --git a/contracts/go/seats/meshwants/contract_test.go b/contracts/go/seats/meshwants/contract_test.go index dcfe1e6c..6a6825e6 100644 --- a/contracts/go/seats/meshwants/contract_test.go +++ b/contracts/go/seats/meshwants/contract_test.go @@ -43,6 +43,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "a version delivered with the machines away", subject: "mesh.seat.mesh-wants.event.version-delivered", given: "{\"away\":[\"laptop\"],\"module\":\"mesh-sdk\",\"parts\":{\"mesh-sdk:go\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},\"v\":1,\"version\":\"0.1.15\"}", wire: "{\"away\":[\"laptop\"],\"module\":\"mesh-sdk\",\"parts\":{\"mesh-sdk:go\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},\"v\":1,\"version\":\"0.1.15\"}", decode: func(b []byte) (any, error) { var v VersionDeliveredEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.version-delivered$")}, + {name: "a composition step's progress", subject: "mesh.seat.mesh-wants.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"assigner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"build\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"assigner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"build\"}", + decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.step-progress\\.[^.*> ]+$")}, {name: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", decode: func(b []byte) (any, error) { var v WantsByWantNodeModuleValue diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 34785b37..7b1eaece 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -33,7 +33,9 @@ const ( ) const ( - EventStepEnded = "step-ended" + EventStepEnded = "step-ended" + EventToolchainReported = "toolchain-reported" + EventStepProgress = "step-progress" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -43,7 +45,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -61,6 +63,28 @@ func SubjectStepEnded(node string) string { return "mesh.seat.node-build-go.even // SubjectStepEndedAll hears every step-ended. const SubjectStepEndedAll = "mesh.seat.node-build-go.event.step-ended.*" +// StreamToolchainReported is the stream toolchain-reported is kept in. +const StreamToolchainReported = "SEAT_node-build-go_toolchain-reported" + +// SubjectToolchainReported is where toolchain-reported is published, on mesh.seat.node-build-go.event.toolchain-reported.. +func SubjectToolchainReported(node string) string { + return "mesh.seat.node-build-go.event.toolchain-reported." + node +} + +// SubjectToolchainReportedAll hears every toolchain-reported. +const SubjectToolchainReportedAll = "mesh.seat.node-build-go.event.toolchain-reported.*" + +// StreamStepProgress is the stream step-progress is kept in. +const StreamStepProgress = "SEAT_node-build-go_step-progress" + +// SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-go.event.step-progress.. +func SubjectStepProgress(node string) string { + return "mesh.seat.node-build-go.event.step-progress." + node +} + +// SubjectStepProgressAll hears every step-progress. +const SubjectStepProgressAll = "mesh.seat.node-build-go.event.step-progress.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-go.accept.delivery" @@ -86,6 +110,47 @@ const ( StateDetectedSteps = "detected-steps" ) +// StepsByAskerNodeIDKey is the key of one value of the state steps under the form "..". +func StepsByAskerNodeIDKey(asker, node, id string) string { return asker + "." + node + "." + id } + +// ParseStepsByAskerNodeIDKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseStepsByAskerNodeIDKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeStepsByAskerNodeIDValue reads one value of the state steps under the form "..", refusing a field StepsByAskerNodeIDValue does not know and any text after the value. +func DecodeStepsByAskerNodeIDValue(raw []byte) (StepsByAskerNodeIDValue, error) { + var v StepsByAskerNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // StepsByNodeIDKey is the key of one value of the state steps under the form ".". func StepsByNodeIDKey(node, id string) string { return node + "." + id } @@ -317,11 +382,21 @@ type BuildAnswer struct { Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } +// BuiltArtifactResolvedItem is a type of the contract. +type BuiltArtifactResolvedItem struct { + // sha256: of the package's bytes as the build fetched them + Digest string `json:"digest"` + Package string `json:"package"` + Version string `json:"version"` +} + // BuiltArtifact is one artifact a build made: its digest and the reference it was published under. type BuiltArtifact struct { Artifact string `json:"artifact"` Digest string `json:"digest"` Reference string `json:"reference,omitempty"` + // one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) + Resolved []BuiltArtifactResolvedItem `json:"resolved,omitempty"` } // Capacity is how many steps the node runs at once. @@ -396,6 +471,16 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchBuildsItem is a type of the contract. +type FetchBuildsItem struct { + Artifact string `json:"artifact"` + // the member's build digest, sha256: + Digest string `json:"digest"` + Module string `json:"module"` + // the name in its ecosystem (the Go module path, the npm name) + Package string `json:"package"` +} + // FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. type FetchPackage struct { Commit string `json:"commit,omitempty"` @@ -411,6 +496,8 @@ type Fetch struct { Base string `json:"base,omitempty"` // the sibling repositories placed beside the fetched tree Beside []FetchBesideItem `json:"beside,omitempty"` + // a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package + Builds []FetchBuildsItem `json:"builds,omitempty"` // the full commit Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths @@ -735,6 +822,22 @@ type StepNames struct { Repositories []string `json:"repositories"` } +// StepProgress is a running ask's progress, said once a minute while the holder runs it (design 47 G section 496). +type StepProgress struct { + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + Phase string `json:"phase"` +} + +// The values of StepProgress's phase. +const ( + StepProgressPhaseFetch = "fetch" + StepProgressPhaseBuild = "build" + StepProgressPhaseRun = "run" + StepProgressPhaseReport = "report" +) + // StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five. type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` @@ -828,8 +931,20 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainReported is a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492). +type ToolchainReported struct { + // the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own + Digest string `json:"digest"` + Language string `json:"language"` + Node string `json:"node"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. type ToolchainUsed struct { + // the building node's digest of its toolchain program (kind 3's entry), never the build module's own program + Digest string `json:"digest,omitempty"` Language string `json:"language"` Recipe string `json:"recipe"` Version string `json:"version"` @@ -912,20 +1027,28 @@ const ( type StepsInput struct { // find them anew, not from the kept answer Again bool `json:"again,omitempty"` - // the artifact reference, : - Artifact string `json:"artifact"` - Fetch Fetch `json:"fetch"` + // the key of detected-steps, . + Artifact string `json:"artifact"` + // delete the artifact's kept key instead of detecting; needs why + Drop bool `json:"drop,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` Module string `json:"module"` + // with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved + Why string `json:"why,omitempty"` } // StepsAnswer is what steps answers. type StepsAnswer struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the artifact whose key a drop deleted + Dropped string `json:"dropped,omitempty"` + // why a drop deleted nothing, naming the hand-act log when it refused + Error string `json:"error,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // answered from the kept answer Kept bool `json:"kept,omitempty"` - Steps []DetectedStep `json:"steps"` + Steps []DetectedStep `json:"steps,omitempty"` } // DeliveryAccept is the body of delivery. @@ -949,6 +1072,15 @@ type CheckRunAccept = StepAsk // StepEndedEvent is the body of the event step-ended. type StepEndedEvent = StepEnded +// ToolchainReportedEvent is the body of the event toolchain-reported. +type ToolchainReportedEvent = ToolchainReported + +// StepProgressEvent is the body of the event step-progress. +type StepProgressEvent = StepProgress + +// StepsByAskerNodeIDValue is one value of the state steps under the key ... +type StepsByAskerNodeIDValue = Step + // StepsByNodeIDValue is one value of the state steps under the key .. type StepsByNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index b35c655d..14aca33e 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -21,6 +21,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-go.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "a node's toolchain reported", subject: "mesh.seat.node-build-go.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.toolchain-reported\\.[^.*> ]+$")}, + {name: "a running ask says its progress", subject: "mesh.seat.node-build-go.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", + decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-progress\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -75,6 +79,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a step keyed under its asker", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v StepsByAskerNodeIDValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "kept-artifact asks", subject: "", given: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", wire: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", @@ -93,6 +103,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "steps answers lint and unit tests", subject: "", given: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", wire: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps drops an artifact's kept steps with a reason", subject: "", given: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", wire: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", + decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop with the artifact", subject: "", given: "{\"dropped\":\"nats.program\"}", wire: "{\"dropped\":\"nats.program\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop that the hand-act log refused", subject: "", given: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", wire: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; 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 00f25faa..9769b994 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -32,7 +32,9 @@ const ( ) const ( - EventStepEnded = "step-ended" + EventStepEnded = "step-ended" + EventToolchainReported = "toolchain-reported" + EventStepProgress = "step-progress" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -42,7 +44,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -62,6 +64,28 @@ func SubjectStepEnded(node string) string { // SubjectStepEndedAll hears every step-ended. const SubjectStepEndedAll = "mesh.seat.node-build-image.event.step-ended.*" +// StreamToolchainReported is the stream toolchain-reported is kept in. +const StreamToolchainReported = "SEAT_node-build-image_toolchain-reported" + +// SubjectToolchainReported is where toolchain-reported is published, on mesh.seat.node-build-image.event.toolchain-reported.. +func SubjectToolchainReported(node string) string { + return "mesh.seat.node-build-image.event.toolchain-reported." + node +} + +// SubjectToolchainReportedAll hears every toolchain-reported. +const SubjectToolchainReportedAll = "mesh.seat.node-build-image.event.toolchain-reported.*" + +// StreamStepProgress is the stream step-progress is kept in. +const StreamStepProgress = "SEAT_node-build-image_step-progress" + +// SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-image.event.step-progress.. +func SubjectStepProgress(node string) string { + return "mesh.seat.node-build-image.event.step-progress." + node +} + +// SubjectStepProgressAll hears every step-progress. +const SubjectStepProgressAll = "mesh.seat.node-build-image.event.step-progress.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-image.accept.delivery" @@ -84,6 +108,47 @@ const ( StateDetectedSteps = "detected-steps" ) +// StepsByAskerNodeIDKey is the key of one value of the state steps under the form "..". +func StepsByAskerNodeIDKey(asker, node, id string) string { return asker + "." + node + "." + id } + +// ParseStepsByAskerNodeIDKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseStepsByAskerNodeIDKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeStepsByAskerNodeIDValue reads one value of the state steps under the form "..", refusing a field StepsByAskerNodeIDValue does not know and any text after the value. +func DecodeStepsByAskerNodeIDValue(raw []byte) (StepsByAskerNodeIDValue, error) { + var v StepsByAskerNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // StepsByNodeIDKey is the key of one value of the state steps under the form ".". func StepsByNodeIDKey(node, id string) string { return node + "." + id } @@ -315,11 +380,21 @@ type BuildAnswer struct { Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } +// BuiltArtifactResolvedItem is a type of the contract. +type BuiltArtifactResolvedItem struct { + // sha256: of the package's bytes as the build fetched them + Digest string `json:"digest"` + Package string `json:"package"` + Version string `json:"version"` +} + // BuiltArtifact is one artifact a build made: its digest and the reference it was published under. type BuiltArtifact struct { Artifact string `json:"artifact"` Digest string `json:"digest"` Reference string `json:"reference,omitempty"` + // one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) + Resolved []BuiltArtifactResolvedItem `json:"resolved,omitempty"` } // Capacity is how many steps the node runs at once. @@ -394,6 +469,16 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchBuildsItem is a type of the contract. +type FetchBuildsItem struct { + Artifact string `json:"artifact"` + // the member's build digest, sha256: + Digest string `json:"digest"` + Module string `json:"module"` + // the name in its ecosystem (the Go module path, the npm name) + Package string `json:"package"` +} + // FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. type FetchPackage struct { Commit string `json:"commit,omitempty"` @@ -409,6 +494,8 @@ type Fetch struct { Base string `json:"base,omitempty"` // the sibling repositories placed beside the fetched tree Beside []FetchBesideItem `json:"beside,omitempty"` + // a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package + Builds []FetchBuildsItem `json:"builds,omitempty"` // the full commit Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths @@ -733,6 +820,22 @@ type StepNames struct { Repositories []string `json:"repositories"` } +// StepProgress is a running ask's progress, said once a minute while the holder runs it (design 47 G section 496). +type StepProgress struct { + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + Phase string `json:"phase"` +} + +// The values of StepProgress's phase. +const ( + StepProgressPhaseFetch = "fetch" + StepProgressPhaseBuild = "build" + StepProgressPhaseRun = "run" + StepProgressPhaseReport = "report" +) + // StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five. type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` @@ -826,8 +929,20 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainReported is a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492). +type ToolchainReported struct { + // the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own + Digest string `json:"digest"` + Language string `json:"language"` + Node string `json:"node"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. type ToolchainUsed struct { + // the building node's digest of its toolchain program (kind 3's entry), never the build module's own program + Digest string `json:"digest,omitempty"` Language string `json:"language"` Recipe string `json:"recipe"` Version string `json:"version"` @@ -910,20 +1025,28 @@ const ( type StepsInput struct { // find them anew, not from the kept answer Again bool `json:"again,omitempty"` - // the artifact reference, : - Artifact string `json:"artifact"` - Fetch Fetch `json:"fetch"` + // the key of detected-steps, . + Artifact string `json:"artifact"` + // delete the artifact's kept key instead of detecting; needs why + Drop bool `json:"drop,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` Module string `json:"module"` + // with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved + Why string `json:"why,omitempty"` } // StepsAnswer is what steps answers. type StepsAnswer struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the artifact whose key a drop deleted + Dropped string `json:"dropped,omitempty"` + // why a drop deleted nothing, naming the hand-act log when it refused + Error string `json:"error,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // answered from the kept answer Kept bool `json:"kept,omitempty"` - Steps []DetectedStep `json:"steps"` + Steps []DetectedStep `json:"steps,omitempty"` } // DeliveryAccept is the body of delivery. @@ -944,6 +1067,15 @@ type RecheckAccept = StepAsk // StepEndedEvent is the body of the event step-ended. type StepEndedEvent = StepEnded +// ToolchainReportedEvent is the body of the event toolchain-reported. +type ToolchainReportedEvent = ToolchainReported + +// StepProgressEvent is the body of the event step-progress. +type StepProgressEvent = StepProgress + +// StepsByAskerNodeIDValue is one value of the state steps under the key ... +type StepsByAskerNodeIDValue = Step + // StepsByNodeIDValue is one value of the state steps under the key .. type StepsByNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildimage/contract_test.go b/contracts/go/seats/nodebuildimage/contract_test.go index fbbbb9c8..3ec60a4f 100644 --- a/contracts/go/seats/nodebuildimage/contract_test.go +++ b/contracts/go/seats/nodebuildimage/contract_test.go @@ -21,6 +21,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "a lint step that could not run, naming the part not heard", subject: "mesh.seat.node-build-image.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "a node's toolchain reported", subject: "mesh.seat.node-build-image.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.toolchain-reported\\.[^.*> ]+$")}, + {name: "a running ask says its progress", subject: "mesh.seat.node-build-image.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", + decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-progress\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -57,6 +61,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a step keyed under its asker", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v StepsByAskerNodeIDValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "kept-artifact asks", subject: "", given: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", wire: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", @@ -75,6 +85,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "steps answers lint and unit tests", subject: "", given: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", wire: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps drops an artifact's kept steps with a reason", subject: "", given: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", wire: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", + decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop with the artifact", subject: "", given: "{\"dropped\":\"nats.program\"}", wire: "{\"dropped\":\"nats.program\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop that the hand-act log refused", subject: "", given: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", wire: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; 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/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 2df88e8f..5004106f 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -32,7 +32,9 @@ const ( ) const ( - EventStepEnded = "step-ended" + EventStepEnded = "step-ended" + EventToolchainReported = "toolchain-reported" + EventStepProgress = "step-progress" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -42,7 +44,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -62,6 +64,28 @@ func SubjectStepEnded(node string) string { // SubjectStepEndedAll hears every step-ended. const SubjectStepEndedAll = "mesh.seat.node-build-node.event.step-ended.*" +// StreamToolchainReported is the stream toolchain-reported is kept in. +const StreamToolchainReported = "SEAT_node-build-node_toolchain-reported" + +// SubjectToolchainReported is where toolchain-reported is published, on mesh.seat.node-build-node.event.toolchain-reported.. +func SubjectToolchainReported(node string) string { + return "mesh.seat.node-build-node.event.toolchain-reported." + node +} + +// SubjectToolchainReportedAll hears every toolchain-reported. +const SubjectToolchainReportedAll = "mesh.seat.node-build-node.event.toolchain-reported.*" + +// StreamStepProgress is the stream step-progress is kept in. +const StreamStepProgress = "SEAT_node-build-node_step-progress" + +// SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-node.event.step-progress.. +func SubjectStepProgress(node string) string { + return "mesh.seat.node-build-node.event.step-progress." + node +} + +// SubjectStepProgressAll hears every step-progress. +const SubjectStepProgressAll = "mesh.seat.node-build-node.event.step-progress.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-node.accept.delivery" @@ -84,6 +108,47 @@ const ( StateDetectedSteps = "detected-steps" ) +// StepsByAskerNodeIDKey is the key of one value of the state steps under the form "..". +func StepsByAskerNodeIDKey(asker, node, id string) string { return asker + "." + node + "." + id } + +// ParseStepsByAskerNodeIDKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseStepsByAskerNodeIDKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeStepsByAskerNodeIDValue reads one value of the state steps under the form "..", refusing a field StepsByAskerNodeIDValue does not know and any text after the value. +func DecodeStepsByAskerNodeIDValue(raw []byte) (StepsByAskerNodeIDValue, error) { + var v StepsByAskerNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // StepsByNodeIDKey is the key of one value of the state steps under the form ".". func StepsByNodeIDKey(node, id string) string { return node + "." + id } @@ -315,11 +380,21 @@ type BuildAnswer struct { Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } +// BuiltArtifactResolvedItem is a type of the contract. +type BuiltArtifactResolvedItem struct { + // sha256: of the package's bytes as the build fetched them + Digest string `json:"digest"` + Package string `json:"package"` + Version string `json:"version"` +} + // BuiltArtifact is one artifact a build made: its digest and the reference it was published under. type BuiltArtifact struct { Artifact string `json:"artifact"` Digest string `json:"digest"` Reference string `json:"reference,omitempty"` + // one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) + Resolved []BuiltArtifactResolvedItem `json:"resolved,omitempty"` } // Capacity is how many steps the node runs at once. @@ -394,6 +469,16 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchBuildsItem is a type of the contract. +type FetchBuildsItem struct { + Artifact string `json:"artifact"` + // the member's build digest, sha256: + Digest string `json:"digest"` + Module string `json:"module"` + // the name in its ecosystem (the Go module path, the npm name) + Package string `json:"package"` +} + // FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. type FetchPackage struct { Commit string `json:"commit,omitempty"` @@ -409,6 +494,8 @@ type Fetch struct { Base string `json:"base,omitempty"` // the sibling repositories placed beside the fetched tree Beside []FetchBesideItem `json:"beside,omitempty"` + // a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package + Builds []FetchBuildsItem `json:"builds,omitempty"` // the full commit Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths @@ -733,6 +820,22 @@ type StepNames struct { Repositories []string `json:"repositories"` } +// StepProgress is a running ask's progress, said once a minute while the holder runs it (design 47 G section 496). +type StepProgress struct { + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + Phase string `json:"phase"` +} + +// The values of StepProgress's phase. +const ( + StepProgressPhaseFetch = "fetch" + StepProgressPhaseBuild = "build" + StepProgressPhaseRun = "run" + StepProgressPhaseReport = "report" +) + // StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five. type StepResult struct { Artifacts []BuiltArtifact `json:"artifacts,omitempty"` @@ -826,8 +929,20 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainReported is a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492). +type ToolchainReported struct { + // the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own + Digest string `json:"digest"` + Language string `json:"language"` + Node string `json:"node"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. type ToolchainUsed struct { + // the building node's digest of its toolchain program (kind 3's entry), never the build module's own program + Digest string `json:"digest,omitempty"` Language string `json:"language"` Recipe string `json:"recipe"` Version string `json:"version"` @@ -910,20 +1025,28 @@ const ( type StepsInput struct { // find them anew, not from the kept answer Again bool `json:"again,omitempty"` - // the artifact reference, : - Artifact string `json:"artifact"` - Fetch Fetch `json:"fetch"` + // the key of detected-steps, . + Artifact string `json:"artifact"` + // delete the artifact's kept key instead of detecting; needs why + Drop bool `json:"drop,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` Module string `json:"module"` + // with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved + Why string `json:"why,omitempty"` } // StepsAnswer is what steps answers. type StepsAnswer struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the artifact whose key a drop deleted + Dropped string `json:"dropped,omitempty"` + // why a drop deleted nothing, naming the hand-act log when it refused + Error string `json:"error,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // answered from the kept answer Kept bool `json:"kept,omitempty"` - Steps []DetectedStep `json:"steps"` + Steps []DetectedStep `json:"steps,omitempty"` } // DeliveryAccept is the body of delivery. @@ -944,6 +1067,15 @@ type RecheckAccept = StepAsk // StepEndedEvent is the body of the event step-ended. type StepEndedEvent = StepEnded +// ToolchainReportedEvent is the body of the event toolchain-reported. +type ToolchainReportedEvent = ToolchainReported + +// StepProgressEvent is the body of the event step-progress. +type StepProgressEvent = StepProgress + +// StepsByAskerNodeIDValue is one value of the state steps under the key ... +type StepsByAskerNodeIDValue = Step + // StepsByNodeIDValue is one value of the state steps under the key .. type StepsByNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go index 982cd713..931da365 100644 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -21,6 +21,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-node.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "a node's toolchain reported", subject: "mesh.seat.node-build-node.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.toolchain-reported\\.[^.*> ]+$")}, + {name: "a running ask says its progress", subject: "mesh.seat.node-build-node.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", + decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-progress\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -61,6 +65,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a step keyed under its asker", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v StepsByAskerNodeIDValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "kept-artifact asks", subject: "", given: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", wire: "{\"digest\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"fp\":\"src2:1480985e\",\"module\":\"artifact-store\"}", @@ -79,6 +89,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "steps answers lint and unit tests", subject: "", given: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", wire: "{\"artifact\":\"nats:program\",\"fingerprint\":\"fp-0123\",\"steps\":[{\"step\":\"lint\"},{\"packages\":[\"./modules/nats\"],\"step\":\"unit\"}]}", decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps drops an artifact's kept steps with a reason", subject: "", given: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", wire: "{\"artifact\":\"nats.program\",\"drop\":true,\"module\":\"nats\",\"why\":\"the detection was wrong\"}", + decode: func(b []byte) (any, error) { var v StepsInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop with the artifact", subject: "", given: "{\"dropped\":\"nats.program\"}", wire: "{\"dropped\":\"nats.program\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "steps answers a drop that the hand-act log refused", subject: "", given: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", wire: "{\"error\":\"the hand-act log refused: nothing was deleted\"}", + decode: func(b []byte) (any, error) { var v StepsAnswer; 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 3aed594f..2179c177 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -159,10 +159,12 @@ }, { "name": "steps", - "description": "Which pipeline steps an artifact can run, found by its language's convention (design 47 *Pipeline steps and the policy*): lint always, unit and integration tests where its packages hold them. Kept by the artifact's fingerprint; again finds them anew.", + "description": "Which pipeline steps an artifact can run, found by its language's convention (design 47 *Pipeline steps and the policy*): lint always, unit and integration tests where its packages hold them. Kept by the artifact's fingerprint; again finds them anew. With drop and why it deletes the artifact's kept key instead, writes the act in the hand-act log of the seat mesh-hand-acts first (a log that refuses deletes nothing and answers error), and answers dropped (design 47 G section 463, section 480).", "callers": [ "seat:mesh-pipeline-runner", - "seat:mesh-catalog" + "seat:mesh-catalog", + "operator", + "sessions" ], "input": { "type": "object", @@ -172,7 +174,7 @@ }, "artifact": { "type": "string", - "description": "the artifact reference, :" + "description": "the key of detected-steps, ." }, "fingerprint": { "type": "string" @@ -183,13 +185,21 @@ "again": { "type": "boolean", "description": "find them anew, not from the kept answer" + }, + "drop": { + "type": "boolean", + "description": "delete the artifact's kept key instead of detecting; needs why" + }, + "why": { + "type": "string", + "description": "with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved" } }, "required": [ "module", - "artifact", - "fetch" - ] + "artifact" + ], + "description": "fetch is required unless drop is given, and why with drop" }, "answer": { "type": "object", @@ -209,12 +219,17 @@ "kept": { "type": "boolean", "description": "answered from the kept answer" + }, + "dropped": { + "type": "string", + "description": "the artifact whose key a drop deleted" + }, + "error": { + "type": "string", + "description": "why a drop deleted nothing, naming the hand-act log when it refused" } }, - "required": [ - "artifact", - "steps" - ] + "required": [] }, "class": "act" } @@ -857,6 +872,35 @@ ] }, "description": "the sibling repositories placed beside the fetched tree" + }, + "builds": { + "type": "array", + "description": "a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string", + "description": "the name in its ecosystem (the Go module path, the npm name)" + }, + "module": { + "type": "string" + }, + "artifact": { + "type": "string" + }, + "digest": { + "type": "string", + "description": "the member's build digest, sha256:" + } + }, + "required": [ + "package", + "module", + "artifact", + "digest" + ] + } } }, "required": [ @@ -875,6 +919,10 @@ }, "recipe": { "type": "string" + }, + "digest": { + "type": "string", + "description": "the building node's digest of its toolchain program (kind 3's entry), never the build module's own program" } }, "required": [ @@ -916,6 +964,30 @@ }, "reference": { "type": "string" + }, + "resolved": { + "type": "array", + "description": "one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1)", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "description": "sha256: of the package's bytes as the build fetched them" + } + }, + "required": [ + "package", + "version", + "digest" + ] + } } }, "required": [ @@ -1628,6 +1700,66 @@ "fingerprint", "steps" ] + }, + "ToolchainReported": { + "type": "object", + "description": "a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492)", + "properties": { + "node": { + "type": "string" + }, + "language": { + "type": "string" + }, + "version": { + "type": "string" + }, + "recipe": { + "type": "string" + }, + "digest": { + "type": "string", + "description": "the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own" + } + }, + "required": [ + "node", + "language", + "version", + "recipe", + "digest" + ] + }, + "StepProgress": { + "type": "object", + "description": "a running ask's progress, said once a minute while the holder runs it (design 47 G section 496)", + "properties": { + "asker": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "phase": { + "type": "string", + "enum": [ + "fetch", + "build", + "run", + "report" + ] + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "asker", + "ask", + "phase", + "at" + ] } }, "events": [ @@ -1641,6 +1773,28 @@ "max-age-days": 90, "max-mib": 128 } + }, + { + "name": "toolchain-reported", + "subject": "toolchain-reported.", + "body": { + "$ref": "#/defs/ToolchainReported" + }, + "keep": { + "max-age-days": 90, + "max-mib": 16 + } + }, + { + "name": "step-progress", + "subject": "step-progress.", + "body": { + "$ref": "#/defs/StepProgress" + }, + "keep": { + "max-age-days": 1, + "max-mib": 16 + } } ] } diff --git a/contracts/seats/mesh-wants/fixtures/events.json b/contracts/seats/mesh-wants/fixtures/events.json index 70fbc77c..7c00e058 100644 --- a/contracts/seats/mesh-wants/fixtures/events.json +++ b/contracts/seats/mesh-wants/fixtures/events.json @@ -166,5 +166,16 @@ "laptop" ] } + }, + { + "name": "a composition step's progress", + "of": "event:step-progress", + "subject": "mesh.seat.mesh-wants.event.step-progress.ace", + "given": { + "asker": "assigner", + "ask": "a1b2", + "phase": "build", + "at": "2026-11-11T10:01:00Z" + } } ] diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json index e002258b..aca8f3b9 100644 --- a/contracts/seats/mesh-wants/seat.json +++ b/contracts/seats/mesh-wants/seat.json @@ -427,6 +427,45 @@ "parts" ] } + }, + { + "name": "step-progress", + "subject": "step-progress.", + "keep": { + "max-age-days": 1, + "max-mib": 16 + }, + "body": { + "type": "object", + "description": "a composition step's progress, said once a minute while the assigner runs it (design 47 G section 496)", + "properties": { + "asker": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "phase": { + "type": "string", + "enum": [ + "fetch", + "build", + "run", + "report" + ] + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "asker", + "ask", + "phase", + "at" + ] + } } ], "family": { diff --git a/contracts/seats/node-build-go/fixtures/events.json b/contracts/seats/node-build-go/fixtures/events.json index 06ad58d6..1d3351e3 100644 --- a/contracts/seats/node-build-go/fixtures/events.json +++ b/contracts/seats/node-build-go/fixtures/events.json @@ -53,5 +53,28 @@ "said": "the sandbox could not fetch novox/mesh-sdk", "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" } + }, + { + "name": "a node's toolchain reported", + "of": "event:toolchain-reported", + "subject": "mesh.seat.node-build-go.event.toolchain-reported.ace", + "given": { + "node": "ace", + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "a running ask says its progress", + "of": "event:step-progress", + "subject": "mesh.seat.node-build-go.event.step-progress.ace", + "given": { + "asker": "runner", + "ask": "a1b2", + "phase": "fetch", + "at": "2026-11-11T10:01:00Z" + } } ] diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 9ee56a47..bb3685b7 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -526,5 +526,17 @@ ], "at": "2026-11-10T10:00:00Z" } + }, + { + "name": "a step keyed under its asker", + "of": "state:steps/..", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-go/fixtures/verbs.json b/contracts/seats/node-build-go/fixtures/verbs.json index 1e4af4dd..7a02ff62 100644 --- a/contracts/seats/node-build-go/fixtures/verbs.json +++ b/contracts/seats/node-build-go/fixtures/verbs.json @@ -100,5 +100,29 @@ } ] } + }, + { + "name": "steps drops an artifact's kept steps with a reason", + "of": "verb:steps.input", + "given": { + "module": "nats", + "artifact": "nats.program", + "drop": true, + "why": "the detection was wrong" + } + }, + { + "name": "steps answers a drop with the artifact", + "of": "verb:steps.answer", + "given": { + "dropped": "nats.program" + } + }, + { + "name": "steps answers a drop that the hand-act log refused", + "of": "verb:steps.answer", + "given": { + "error": "the hand-act log refused: nothing was deleted" + } } ] diff --git a/contracts/seats/node-build-go/seat.json b/contracts/seats/node-build-go/seat.json index 4d30fcd7..f9be14f8 100644 --- a/contracts/seats/node-build-go/seat.json +++ b/contracts/seats/node-build-go/seat.json @@ -87,12 +87,19 @@ "name": "steps", "template": "state", "key-forms": [ + { + "key": "..", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node, keyed by the asker the bus proves (design 47 G section 467, section 481)" + }, { "key": ".", "value": { "$ref": "#/defs/Step" }, - "description": "a step on a node" + "description": "a step whose ask came on main's subject, with no asker's name (design 47 G section 483)" } ], "description": "the steps this module runs", diff --git a/contracts/seats/node-build-image/fixtures/events.json b/contracts/seats/node-build-image/fixtures/events.json index 1e20d2ed..e8dca58f 100644 --- a/contracts/seats/node-build-image/fixtures/events.json +++ b/contracts/seats/node-build-image/fixtures/events.json @@ -36,5 +36,28 @@ "said": "the sandbox could not fetch novox/mesh-sdk", "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" } + }, + { + "name": "a node's toolchain reported", + "of": "event:toolchain-reported", + "subject": "mesh.seat.node-build-image.event.toolchain-reported.ace", + "given": { + "node": "ace", + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "a running ask says its progress", + "of": "event:step-progress", + "subject": "mesh.seat.node-build-image.event.step-progress.ace", + "given": { + "asker": "runner", + "ask": "a1b2", + "phase": "fetch", + "at": "2026-11-11T10:01:00Z" + } } ] diff --git a/contracts/seats/node-build-image/fixtures/states.json b/contracts/seats/node-build-image/fixtures/states.json index 3cbef09e..149ea1ca 100644 --- a/contracts/seats/node-build-image/fixtures/states.json +++ b/contracts/seats/node-build-image/fixtures/states.json @@ -186,5 +186,17 @@ ], "at": "2026-11-10T10:00:00Z" } + }, + { + "name": "a step keyed under its asker", + "of": "state:steps/..", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-image/fixtures/verbs.json b/contracts/seats/node-build-image/fixtures/verbs.json index 1e4af4dd..7a02ff62 100644 --- a/contracts/seats/node-build-image/fixtures/verbs.json +++ b/contracts/seats/node-build-image/fixtures/verbs.json @@ -100,5 +100,29 @@ } ] } + }, + { + "name": "steps drops an artifact's kept steps with a reason", + "of": "verb:steps.input", + "given": { + "module": "nats", + "artifact": "nats.program", + "drop": true, + "why": "the detection was wrong" + } + }, + { + "name": "steps answers a drop with the artifact", + "of": "verb:steps.answer", + "given": { + "dropped": "nats.program" + } + }, + { + "name": "steps answers a drop that the hand-act log refused", + "of": "verb:steps.answer", + "given": { + "error": "the hand-act log refused: nothing was deleted" + } } ] diff --git a/contracts/seats/node-build-image/seat.json b/contracts/seats/node-build-image/seat.json index 2d5f83ab..5528da1f 100644 --- a/contracts/seats/node-build-image/seat.json +++ b/contracts/seats/node-build-image/seat.json @@ -65,12 +65,19 @@ "name": "steps", "template": "state", "key-forms": [ + { + "key": "..", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node, keyed by the asker the bus proves (design 47 G section 467, section 481)" + }, { "key": ".", "value": { "$ref": "#/defs/Step" }, - "description": "a step on a node" + "description": "a step whose ask came on main's subject, with no asker's name (design 47 G section 483)" } ], "description": "the steps this module runs", diff --git a/contracts/seats/node-build-node/fixtures/events.json b/contracts/seats/node-build-node/fixtures/events.json index 0fb1ac2d..1f1924a0 100644 --- a/contracts/seats/node-build-node/fixtures/events.json +++ b/contracts/seats/node-build-node/fixtures/events.json @@ -53,5 +53,28 @@ "said": "the sandbox could not fetch novox/mesh-sdk", "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" } + }, + { + "name": "a node's toolchain reported", + "of": "event:toolchain-reported", + "subject": "mesh.seat.node-build-node.event.toolchain-reported.ace", + "given": { + "node": "ace", + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "a running ask says its progress", + "of": "event:step-progress", + "subject": "mesh.seat.node-build-node.event.step-progress.ace", + "given": { + "asker": "runner", + "ask": "a1b2", + "phase": "fetch", + "at": "2026-11-11T10:01:00Z" + } } ] diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json index ccf06869..e077d0d6 100644 --- a/contracts/seats/node-build-node/fixtures/states.json +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -274,5 +274,17 @@ ], "at": "2026-11-10T10:00:00Z" } + }, + { + "name": "a step keyed under its asker", + "of": "state:steps/..", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-node/fixtures/verbs.json b/contracts/seats/node-build-node/fixtures/verbs.json index 1e4af4dd..7a02ff62 100644 --- a/contracts/seats/node-build-node/fixtures/verbs.json +++ b/contracts/seats/node-build-node/fixtures/verbs.json @@ -100,5 +100,29 @@ } ] } + }, + { + "name": "steps drops an artifact's kept steps with a reason", + "of": "verb:steps.input", + "given": { + "module": "nats", + "artifact": "nats.program", + "drop": true, + "why": "the detection was wrong" + } + }, + { + "name": "steps answers a drop with the artifact", + "of": "verb:steps.answer", + "given": { + "dropped": "nats.program" + } + }, + { + "name": "steps answers a drop that the hand-act log refused", + "of": "verb:steps.answer", + "given": { + "error": "the hand-act log refused: nothing was deleted" + } } ] diff --git a/contracts/seats/node-build-node/seat.json b/contracts/seats/node-build-node/seat.json index 978f9dde..910ff77c 100644 --- a/contracts/seats/node-build-node/seat.json +++ b/contracts/seats/node-build-node/seat.json @@ -72,12 +72,19 @@ "name": "steps", "template": "state", "key-forms": [ + { + "key": "..", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node, keyed by the asker the bus proves (design 47 G section 467, section 481)" + }, { "key": ".", "value": { "$ref": "#/defs/Step" }, - "description": "a step on a node" + "description": "a step whose ask came on main's subject, with no asker's name (design 47 G section 483)" } ], "description": "the steps this module runs", diff --git a/contracts/ts/seats/meshwants/index.d.ts b/contracts/ts/seats/meshwants/index.d.ts index 804dc8c1..a2760e6c 100644 --- a/contracts/ts/seats/meshwants/index.d.ts +++ b/contracts/ts/seats/meshwants/index.d.ts @@ -29,6 +29,7 @@ export declare const events: { readonly room: "room"; readonly stepEnded: "step-ended"; readonly versionDelivered: "version-delivered"; + readonly stepProgress: "step-progress"; }; export declare const family: { readonly name: "mesh-wants.events"; readonly maxAgeDays: 7 }; @@ -61,6 +62,12 @@ export declare const versionDeliveredStream: "SEAT_mesh-wants_version-delivered" export declare const versionDeliveredSubject: "mesh.seat.mesh-wants.event.version-delivered"; +export declare const stepProgressStream: "SEAT_mesh-wants_step-progress"; + +export declare function stepProgressSubject(node: string): string; + +export declare const stepProgressSubjectAll: "mesh.seat.mesh-wants.event.step-progress.*"; + export declare const stepSubject: "mesh.seat.mesh-wants.accept.step"; export declare const states: { @@ -474,6 +481,14 @@ export interface VersionDeliveredEvent { "version": string; } +/** is the body of the event step-progress. */ +export interface StepProgressEvent { + "ask": string; + "asker": string; + "at": string; + "phase": "fetch" | "build" | "run" | "report"; +} + /** is one value of the state wants under the key want... */ export interface WantsByWantNodeModuleValue { /** again or skip */ diff --git a/contracts/ts/seats/meshwants/index.js b/contracts/ts/seats/meshwants/index.js index 96565037..488f1228 100644 --- a/contracts/ts/seats/meshwants/index.js +++ b/contracts/ts/seats/meshwants/index.js @@ -29,6 +29,7 @@ export const events = { room: "room", stepEnded: "step-ended", versionDelivered: "version-delivered", + stepProgress: "step-progress", }; export const family = { name: "mesh-wants.events", maxAgeDays: 7 }; @@ -63,6 +64,14 @@ export const versionDeliveredStream = "SEAT_mesh-wants_version-delivered"; export const versionDeliveredSubject = "mesh.seat.mesh-wants.event.version-delivered"; +export const stepProgressStream = "SEAT_mesh-wants_step-progress"; + +export function stepProgressSubject(node) { + return "mesh.seat.mesh-wants.event.step-progress." + node; +} + +export const stepProgressSubjectAll = "mesh.seat.mesh-wants.event.step-progress.*"; + export const stepSubject = "mesh.seat.mesh-wants.accept.step"; export const states = { diff --git a/contracts/ts/seats/nodebuildgo/index.d.ts b/contracts/ts/seats/nodebuildgo/index.d.ts index ff6e8d0e..ac126e27 100644 --- a/contracts/ts/seats/nodebuildgo/index.d.ts +++ b/contracts/ts/seats/nodebuildgo/index.d.ts @@ -26,6 +26,8 @@ export declare const accepts: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; export declare const family: { readonly name: "node-build-go.events"; readonly maxAgeDays: 7 }; @@ -36,6 +38,18 @@ export declare function stepEndedSubject(node: string): string; export declare const stepEndedSubjectAll: "mesh.seat.node-build-go.event.step-ended.*"; +export declare const toolchainReportedStream: "SEAT_node-build-go_toolchain-reported"; + +export declare function toolchainReportedSubject(node: string): string; + +export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-go.event.toolchain-reported.*"; + +export declare const stepProgressStream: "SEAT_node-build-go_step-progress"; + +export declare function stepProgressSubject(node: string): string; + +export declare const stepProgressSubjectAll: "mesh.seat.node-build-go.event.step-progress.*"; + export declare const deliverySubject: "mesh.seat.node-build-go.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-go.accept.check"; @@ -55,6 +69,12 @@ export declare const states: { readonly detectedSteps: "detected-steps"; }; +/** The key of one value of the state steps under the form "..". */ +export declare function stepsByAskerNodeIDKey(asker: string, node: string, id: string): string; + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export declare function parseStepsByAskerNodeIDKey(_key: string): { asker: string; node: string; id: string } | undefined; + /** The key of one value of the state steps under the form ".". */ export declare function stepsByNodeIDKey(node: string, id: string): string; @@ -94,6 +114,7 @@ export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { m export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; +export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; @@ -107,6 +128,7 @@ export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchBuildsItem = buildStep.FetchBuildsItem; export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type KeptArtifactAnswer = buildStep.KeptArtifactAnswer; @@ -139,6 +161,8 @@ export type StepAskCompose = buildStep.StepAskCompose; export type StepEnded = buildStep.StepEnded; export type StepEndedEvent = buildStep.StepEndedEvent; export type StepNames = buildStep.StepNames; +export type StepProgress = buildStep.StepProgress; +export type StepProgressEvent = buildStep.StepProgressEvent; export type StepResult = buildStep.StepResult; export type StepRun = buildStep.StepRun; export type StepsAnswer = buildStep.StepsAnswer; @@ -146,6 +170,8 @@ export type StepsInput = buildStep.StepsInput; export type TakenVersion = buildStep.TakenVersion; export type TestCounts = buildStep.TestCounts; export type Toolchain = buildStep.Toolchain; +export type ToolchainReported = buildStep.ToolchainReported; +export type ToolchainReportedEvent = buildStep.ToolchainReportedEvent; export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ @@ -166,6 +192,9 @@ export type RecheckAccept = StepAsk; /** is the body of check.run. */ export type CheckRunAccept = StepAsk; +/** is one value of the state steps under the key ... */ +export type StepsByAskerNodeIDValue = Step; + /** is one value of the state steps under the key .. */ export type StepsByNodeIDValue = Step; diff --git a/contracts/ts/seats/nodebuildgo/index.js b/contracts/ts/seats/nodebuildgo/index.js index e80d7956..cf4a3f99 100644 --- a/contracts/ts/seats/nodebuildgo/index.js +++ b/contracts/ts/seats/nodebuildgo/index.js @@ -24,6 +24,8 @@ export const accepts = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export const family = { name: "node-build-go.events", maxAgeDays: 7 }; @@ -36,6 +38,22 @@ export function stepEndedSubject(node) { export const stepEndedSubjectAll = "mesh.seat.node-build-go.event.step-ended.*"; +export const toolchainReportedStream = "SEAT_node-build-go_toolchain-reported"; + +export function toolchainReportedSubject(node) { + return "mesh.seat.node-build-go.event.toolchain-reported." + node; +} + +export const toolchainReportedSubjectAll = "mesh.seat.node-build-go.event.toolchain-reported.*"; + +export const stepProgressStream = "SEAT_node-build-go_step-progress"; + +export function stepProgressSubject(node) { + return "mesh.seat.node-build-go.event.step-progress." + node; +} + +export const stepProgressSubjectAll = "mesh.seat.node-build-go.event.step-progress.*"; + export const deliverySubject = "mesh.seat.node-build-go.accept.delivery"; export const checkSubject = "mesh.seat.node-build-go.accept.check"; @@ -55,6 +73,29 @@ export const states = { detectedSteps: "detected-steps", }; +/** The key of one value of the state steps under the form "..". */ +export function stepsByAskerNodeIDKey(asker, node, id) { + return asker + "." + node + "." + id; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseStepsByAskerNodeIDKey(_key) { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const asker = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const node = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { asker, node, id }; +} + /** The key of one value of the state steps under the form ".". */ export function stepsByNodeIDKey(node, id) { return node + "." + id; diff --git a/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts b/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts index 3a3c18ba..a6cc4551 100644 --- a/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts @@ -13,6 +13,8 @@ export declare const verbs: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -32,11 +34,20 @@ export interface BuildAnswer { "toolchain"?: ToolchainUsed; } +export interface BuiltArtifactResolvedItem { + /** sha256: of the package's bytes as the build fetched them */ + "digest": string; + "package": string; + "version": string; +} + /** one artifact a build made: its digest and the reference it was published under */ export interface BuiltArtifact { "artifact": string; "digest": string; "reference"?: string; + /** one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) */ + "resolved"?: BuiltArtifactResolvedItem[]; } /** how many steps the node runs at once */ @@ -100,6 +111,15 @@ export interface FetchBesideItem { "repository": string; } +export interface FetchBuildsItem { + "artifact": string; + /** the member's build digest, sha256: */ + "digest": string; + "module": string; + /** the name in its ecosystem (the Go module path, the npm name) */ + "package": string; +} + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ export interface FetchPackage { "commit"?: string; @@ -115,6 +135,8 @@ export interface Fetch { "base"?: string; /** the sibling repositories placed beside the fetched tree */ "beside"?: FetchBesideItem[]; + /** a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package */ + "builds"?: FetchBuildsItem[]; /** the full commit */ "commit": string; /** the module's directory in the repository ("." for its root), with paths */ @@ -416,6 +438,14 @@ export interface StepNames { "repositories": string[]; } +/** a running ask's progress, said once a minute while the holder runs it (design 47 G section 496) */ +export interface StepProgress { + "ask": string; + "asker": string; + "at": string; + "phase": "fetch" | "build" | "run" | "report"; +} + /** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five */ export interface StepResult { "artifacts"?: BuiltArtifact[]; @@ -502,8 +532,20 @@ export interface Toolchain { "version": string; } +/** a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492) */ +export interface ToolchainReported { + /** the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own */ + "digest": string; + "language": string; + "node": string; + "recipe": string; + "version": string; +} + /** the toolchain a build was made with: the language, the installed version and the build module's recipe */ export interface ToolchainUsed { + /** the building node's digest of its toolchain program (kind 3's entry), never the build module's own program */ + "digest"?: string; "language": string; "recipe": string; "version": string; @@ -579,22 +621,36 @@ export interface KeptArtifactAnswer { export interface StepsInput { /** find them anew, not from the kept answer */ "again"?: boolean; - /** the artifact reference, : */ + /** the key of detected-steps, . */ "artifact": string; - "fetch": Fetch; + /** delete the artifact's kept key instead of detecting; needs why */ + "drop"?: boolean; + "fetch"?: Fetch; "fingerprint"?: string; "module": string; + /** with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved */ + "why"?: string; } /** is what steps answers. */ export interface StepsAnswer { - "artifact": string; + "artifact"?: string; + /** the artifact whose key a drop deleted */ + "dropped"?: string; + /** why a drop deleted nothing, naming the hand-act log when it refused */ + "error"?: string; "fingerprint"?: string; /** answered from the kept answer */ "kept"?: boolean; - "steps": DetectedStep[]; + "steps"?: DetectedStep[]; } /** is the body of the event step-ended. */ export type StepEndedEvent = StepEnded; +/** is the body of the event toolchain-reported. */ +export type ToolchainReportedEvent = ToolchainReported; + +/** is the body of the event step-progress. */ +export type StepProgressEvent = StepProgress; + diff --git a/contracts/ts/seats/nodebuildgo/parts/build-step.js b/contracts/ts/seats/nodebuildgo/parts/build-step.js index 027da325..155a68be 100644 --- a/contracts/ts/seats/nodebuildgo/parts/build-step.js +++ b/contracts/ts/seats/nodebuildgo/parts/build-step.js @@ -13,6 +13,8 @@ export const verbs = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export {}; diff --git a/contracts/ts/seats/nodebuildimage/index.d.ts b/contracts/ts/seats/nodebuildimage/index.d.ts index 2cfdc13b..06659b29 100644 --- a/contracts/ts/seats/nodebuildimage/index.d.ts +++ b/contracts/ts/seats/nodebuildimage/index.d.ts @@ -25,6 +25,8 @@ export declare const accepts: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; export declare const family: { readonly name: "node-build-image.events"; readonly maxAgeDays: 7 }; @@ -35,6 +37,18 @@ export declare function stepEndedSubject(node: string): string; export declare const stepEndedSubjectAll: "mesh.seat.node-build-image.event.step-ended.*"; +export declare const toolchainReportedStream: "SEAT_node-build-image_toolchain-reported"; + +export declare function toolchainReportedSubject(node: string): string; + +export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-image.event.toolchain-reported.*"; + +export declare const stepProgressStream: "SEAT_node-build-image_step-progress"; + +export declare function stepProgressSubject(node: string): string; + +export declare const stepProgressSubjectAll: "mesh.seat.node-build-image.event.step-progress.*"; + export declare const deliverySubject: "mesh.seat.node-build-image.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-image.accept.check"; @@ -52,6 +66,12 @@ export declare const states: { readonly detectedSteps: "detected-steps"; }; +/** The key of one value of the state steps under the form "..". */ +export declare function stepsByAskerNodeIDKey(asker: string, node: string, id: string): string; + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export declare function parseStepsByAskerNodeIDKey(_key: string): { asker: string; node: string; id: string } | undefined; + /** The key of one value of the state steps under the form ".". */ export declare function stepsByNodeIDKey(node: string, id: string): string; @@ -91,6 +111,7 @@ export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { m export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; +export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; @@ -104,6 +125,7 @@ export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchBuildsItem = buildStep.FetchBuildsItem; export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type KeptArtifactAnswer = buildStep.KeptArtifactAnswer; @@ -136,6 +158,8 @@ export type StepAskCompose = buildStep.StepAskCompose; export type StepEnded = buildStep.StepEnded; export type StepEndedEvent = buildStep.StepEndedEvent; export type StepNames = buildStep.StepNames; +export type StepProgress = buildStep.StepProgress; +export type StepProgressEvent = buildStep.StepProgressEvent; export type StepResult = buildStep.StepResult; export type StepRun = buildStep.StepRun; export type StepsAnswer = buildStep.StepsAnswer; @@ -143,6 +167,8 @@ export type StepsInput = buildStep.StepsInput; export type TakenVersion = buildStep.TakenVersion; export type TestCounts = buildStep.TestCounts; export type Toolchain = buildStep.Toolchain; +export type ToolchainReported = buildStep.ToolchainReported; +export type ToolchainReportedEvent = buildStep.ToolchainReportedEvent; export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ @@ -160,6 +186,9 @@ export type DeliveryStepAccept = StepAsk; /** is the body of recheck. */ export type RecheckAccept = StepAsk; +/** is one value of the state steps under the key ... */ +export type StepsByAskerNodeIDValue = Step; + /** is one value of the state steps under the key .. */ export type StepsByNodeIDValue = Step; diff --git a/contracts/ts/seats/nodebuildimage/index.js b/contracts/ts/seats/nodebuildimage/index.js index 6df298da..8a23cc70 100644 --- a/contracts/ts/seats/nodebuildimage/index.js +++ b/contracts/ts/seats/nodebuildimage/index.js @@ -23,6 +23,8 @@ export const accepts = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export const family = { name: "node-build-image.events", maxAgeDays: 7 }; @@ -35,6 +37,22 @@ export function stepEndedSubject(node) { export const stepEndedSubjectAll = "mesh.seat.node-build-image.event.step-ended.*"; +export const toolchainReportedStream = "SEAT_node-build-image_toolchain-reported"; + +export function toolchainReportedSubject(node) { + return "mesh.seat.node-build-image.event.toolchain-reported." + node; +} + +export const toolchainReportedSubjectAll = "mesh.seat.node-build-image.event.toolchain-reported.*"; + +export const stepProgressStream = "SEAT_node-build-image_step-progress"; + +export function stepProgressSubject(node) { + return "mesh.seat.node-build-image.event.step-progress." + node; +} + +export const stepProgressSubjectAll = "mesh.seat.node-build-image.event.step-progress.*"; + export const deliverySubject = "mesh.seat.node-build-image.accept.delivery"; export const checkSubject = "mesh.seat.node-build-image.accept.check"; @@ -52,6 +70,29 @@ export const states = { detectedSteps: "detected-steps", }; +/** The key of one value of the state steps under the form "..". */ +export function stepsByAskerNodeIDKey(asker, node, id) { + return asker + "." + node + "." + id; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseStepsByAskerNodeIDKey(_key) { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const asker = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const node = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { asker, node, id }; +} + /** The key of one value of the state steps under the form ".". */ export function stepsByNodeIDKey(node, id) { return node + "." + id; diff --git a/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts b/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts index 3a3c18ba..a6cc4551 100644 --- a/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts @@ -13,6 +13,8 @@ export declare const verbs: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -32,11 +34,20 @@ export interface BuildAnswer { "toolchain"?: ToolchainUsed; } +export interface BuiltArtifactResolvedItem { + /** sha256: of the package's bytes as the build fetched them */ + "digest": string; + "package": string; + "version": string; +} + /** one artifact a build made: its digest and the reference it was published under */ export interface BuiltArtifact { "artifact": string; "digest": string; "reference"?: string; + /** one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) */ + "resolved"?: BuiltArtifactResolvedItem[]; } /** how many steps the node runs at once */ @@ -100,6 +111,15 @@ export interface FetchBesideItem { "repository": string; } +export interface FetchBuildsItem { + "artifact": string; + /** the member's build digest, sha256: */ + "digest": string; + "module": string; + /** the name in its ecosystem (the Go module path, the npm name) */ + "package": string; +} + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ export interface FetchPackage { "commit"?: string; @@ -115,6 +135,8 @@ export interface Fetch { "base"?: string; /** the sibling repositories placed beside the fetched tree */ "beside"?: FetchBesideItem[]; + /** a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package */ + "builds"?: FetchBuildsItem[]; /** the full commit */ "commit": string; /** the module's directory in the repository ("." for its root), with paths */ @@ -416,6 +438,14 @@ export interface StepNames { "repositories": string[]; } +/** a running ask's progress, said once a minute while the holder runs it (design 47 G section 496) */ +export interface StepProgress { + "ask": string; + "asker": string; + "at": string; + "phase": "fetch" | "build" | "run" | "report"; +} + /** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five */ export interface StepResult { "artifacts"?: BuiltArtifact[]; @@ -502,8 +532,20 @@ export interface Toolchain { "version": string; } +/** a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492) */ +export interface ToolchainReported { + /** the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own */ + "digest": string; + "language": string; + "node": string; + "recipe": string; + "version": string; +} + /** the toolchain a build was made with: the language, the installed version and the build module's recipe */ export interface ToolchainUsed { + /** the building node's digest of its toolchain program (kind 3's entry), never the build module's own program */ + "digest"?: string; "language": string; "recipe": string; "version": string; @@ -579,22 +621,36 @@ export interface KeptArtifactAnswer { export interface StepsInput { /** find them anew, not from the kept answer */ "again"?: boolean; - /** the artifact reference, : */ + /** the key of detected-steps, . */ "artifact": string; - "fetch": Fetch; + /** delete the artifact's kept key instead of detecting; needs why */ + "drop"?: boolean; + "fetch"?: Fetch; "fingerprint"?: string; "module": string; + /** with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved */ + "why"?: string; } /** is what steps answers. */ export interface StepsAnswer { - "artifact": string; + "artifact"?: string; + /** the artifact whose key a drop deleted */ + "dropped"?: string; + /** why a drop deleted nothing, naming the hand-act log when it refused */ + "error"?: string; "fingerprint"?: string; /** answered from the kept answer */ "kept"?: boolean; - "steps": DetectedStep[]; + "steps"?: DetectedStep[]; } /** is the body of the event step-ended. */ export type StepEndedEvent = StepEnded; +/** is the body of the event toolchain-reported. */ +export type ToolchainReportedEvent = ToolchainReported; + +/** is the body of the event step-progress. */ +export type StepProgressEvent = StepProgress; + diff --git a/contracts/ts/seats/nodebuildimage/parts/build-step.js b/contracts/ts/seats/nodebuildimage/parts/build-step.js index 027da325..155a68be 100644 --- a/contracts/ts/seats/nodebuildimage/parts/build-step.js +++ b/contracts/ts/seats/nodebuildimage/parts/build-step.js @@ -13,6 +13,8 @@ export const verbs = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export {}; diff --git a/contracts/ts/seats/nodebuildnode/index.d.ts b/contracts/ts/seats/nodebuildnode/index.d.ts index 3a87dc86..0d4743ec 100644 --- a/contracts/ts/seats/nodebuildnode/index.d.ts +++ b/contracts/ts/seats/nodebuildnode/index.d.ts @@ -25,6 +25,8 @@ export declare const accepts: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; export declare const family: { readonly name: "node-build-node.events"; readonly maxAgeDays: 7 }; @@ -35,6 +37,18 @@ export declare function stepEndedSubject(node: string): string; export declare const stepEndedSubjectAll: "mesh.seat.node-build-node.event.step-ended.*"; +export declare const toolchainReportedStream: "SEAT_node-build-node_toolchain-reported"; + +export declare function toolchainReportedSubject(node: string): string; + +export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-node.event.toolchain-reported.*"; + +export declare const stepProgressStream: "SEAT_node-build-node_step-progress"; + +export declare function stepProgressSubject(node: string): string; + +export declare const stepProgressSubjectAll: "mesh.seat.node-build-node.event.step-progress.*"; + export declare const deliverySubject: "mesh.seat.node-build-node.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-node.accept.check"; @@ -52,6 +66,12 @@ export declare const states: { readonly detectedSteps: "detected-steps"; }; +/** The key of one value of the state steps under the form "..". */ +export declare function stepsByAskerNodeIDKey(asker: string, node: string, id: string): string; + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export declare function parseStepsByAskerNodeIDKey(_key: string): { asker: string; node: string; id: string } | undefined; + /** The key of one value of the state steps under the form ".". */ export declare function stepsByNodeIDKey(node: string, id: string): string; @@ -91,6 +111,7 @@ export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { m export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; +export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; export type ComposeAnswer = buildStep.ComposeAnswer; @@ -104,6 +125,7 @@ export type DropInput = buildStep.DropInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchBuildsItem = buildStep.FetchBuildsItem; export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type KeptArtifactAnswer = buildStep.KeptArtifactAnswer; @@ -136,6 +158,8 @@ export type StepAskCompose = buildStep.StepAskCompose; export type StepEnded = buildStep.StepEnded; export type StepEndedEvent = buildStep.StepEndedEvent; export type StepNames = buildStep.StepNames; +export type StepProgress = buildStep.StepProgress; +export type StepProgressEvent = buildStep.StepProgressEvent; export type StepResult = buildStep.StepResult; export type StepRun = buildStep.StepRun; export type StepsAnswer = buildStep.StepsAnswer; @@ -143,6 +167,8 @@ export type StepsInput = buildStep.StepsInput; export type TakenVersion = buildStep.TakenVersion; export type TestCounts = buildStep.TestCounts; export type Toolchain = buildStep.Toolchain; +export type ToolchainReported = buildStep.ToolchainReported; +export type ToolchainReportedEvent = buildStep.ToolchainReportedEvent; export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ @@ -160,6 +186,9 @@ export type DeliveryStepAccept = StepAsk; /** is the body of recheck. */ export type RecheckAccept = StepAsk; +/** is one value of the state steps under the key ... */ +export type StepsByAskerNodeIDValue = Step; + /** is one value of the state steps under the key .. */ export type StepsByNodeIDValue = Step; diff --git a/contracts/ts/seats/nodebuildnode/index.js b/contracts/ts/seats/nodebuildnode/index.js index 3c9047d8..6cda8ea2 100644 --- a/contracts/ts/seats/nodebuildnode/index.js +++ b/contracts/ts/seats/nodebuildnode/index.js @@ -23,6 +23,8 @@ export const accepts = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export const family = { name: "node-build-node.events", maxAgeDays: 7 }; @@ -35,6 +37,22 @@ export function stepEndedSubject(node) { export const stepEndedSubjectAll = "mesh.seat.node-build-node.event.step-ended.*"; +export const toolchainReportedStream = "SEAT_node-build-node_toolchain-reported"; + +export function toolchainReportedSubject(node) { + return "mesh.seat.node-build-node.event.toolchain-reported." + node; +} + +export const toolchainReportedSubjectAll = "mesh.seat.node-build-node.event.toolchain-reported.*"; + +export const stepProgressStream = "SEAT_node-build-node_step-progress"; + +export function stepProgressSubject(node) { + return "mesh.seat.node-build-node.event.step-progress." + node; +} + +export const stepProgressSubjectAll = "mesh.seat.node-build-node.event.step-progress.*"; + export const deliverySubject = "mesh.seat.node-build-node.accept.delivery"; export const checkSubject = "mesh.seat.node-build-node.accept.check"; @@ -52,6 +70,29 @@ export const states = { detectedSteps: "detected-steps", }; +/** The key of one value of the state steps under the form "..". */ +export function stepsByAskerNodeIDKey(asker, node, id) { + return asker + "." + node + "." + id; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseStepsByAskerNodeIDKey(_key) { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const asker = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const node = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; + return { asker, node, id }; +} + /** The key of one value of the state steps under the form ".". */ export function stepsByNodeIDKey(node, id) { return node + "." + id; diff --git a/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts b/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts index 3a3c18ba..a6cc4551 100644 --- a/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts @@ -13,6 +13,8 @@ export declare const verbs: { export declare const events: { readonly stepEnded: "step-ended"; + readonly toolchainReported: "toolchain-reported"; + readonly stepProgress: "step-progress"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -32,11 +34,20 @@ export interface BuildAnswer { "toolchain"?: ToolchainUsed; } +export interface BuiltArtifactResolvedItem { + /** sha256: of the package's bytes as the build fetched them */ + "digest": string; + "package": string; + "version": string; +} + /** one artifact a build made: its digest and the reference it was published under */ export interface BuiltArtifact { "artifact": string; "digest": string; "reference"?: string; + /** one per mesh package the build resolved (the catalog graph entry's packages, the edges of kind 1) */ + "resolved"?: BuiltArtifactResolvedItem[]; } /** how many steps the node runs at once */ @@ -100,6 +111,15 @@ export interface FetchBesideItem { "repository": string; } +export interface FetchBuildsItem { + "artifact": string; + /** the member's build digest, sha256: */ + "digest": string; + "module": string; + /** the name in its ecosystem (the Go module path, the npm name) */ + "package": string; +} + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ export interface FetchPackage { "commit"?: string; @@ -115,6 +135,8 @@ export interface Fetch { "base"?: string; /** the sibling repositories placed beside the fetched tree */ "beside"?: FetchBesideItem[]; + /** a group run's package members, each by its build's digest: the build module reads each from the artifact store at artifact-store:////blobs/, checks the bytes against the digest and lays them as the group package; a package named here is named in neither packages nor package */ + "builds"?: FetchBuildsItem[]; /** the full commit */ "commit": string; /** the module's directory in the repository ("." for its root), with paths */ @@ -416,6 +438,14 @@ export interface StepNames { "repositories": string[]; } +/** a running ask's progress, said once a minute while the holder runs it (design 47 G section 496) */ +export interface StepProgress { + "ask": string; + "asker": string; + "at": string; + "phase": "fetch" | "build" | "run" | "report"; +} + /** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports, out-of-step modules and the artifact it kept; a check's fetch and levels; a compose's built, key and verdict; a run's built, key, exit, output and said; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the five */ export interface StepResult { "artifacts"?: BuiltArtifact[]; @@ -502,8 +532,20 @@ export interface Toolchain { "version": string; } +/** a node's toolchain of a language, said at the build module's start and each time its toolchains key of the language changes (design 47 G section 492) */ +export interface ToolchainReported { + /** the digest of the program of the provision the build module requires for that language's toolchain, never the build module's own */ + "digest": string; + "language": string; + "node": string; + "recipe": string; + "version": string; +} + /** the toolchain a build was made with: the language, the installed version and the build module's recipe */ export interface ToolchainUsed { + /** the building node's digest of its toolchain program (kind 3's entry), never the build module's own program */ + "digest"?: string; "language": string; "recipe": string; "version": string; @@ -579,22 +621,36 @@ export interface KeptArtifactAnswer { export interface StepsInput { /** find them anew, not from the kept answer */ "again"?: boolean; - /** the artifact reference, : */ + /** the key of detected-steps, . */ "artifact": string; - "fetch": Fetch; + /** delete the artifact's kept key instead of detecting; needs why */ + "drop"?: boolean; + "fetch"?: Fetch; "fingerprint"?: string; "module": string; + /** with drop, why: a non-empty text, written in the hand-act log with the caller the bus proved */ + "why"?: string; } /** is what steps answers. */ export interface StepsAnswer { - "artifact": string; + "artifact"?: string; + /** the artifact whose key a drop deleted */ + "dropped"?: string; + /** why a drop deleted nothing, naming the hand-act log when it refused */ + "error"?: string; "fingerprint"?: string; /** answered from the kept answer */ "kept"?: boolean; - "steps": DetectedStep[]; + "steps"?: DetectedStep[]; } /** is the body of the event step-ended. */ export type StepEndedEvent = StepEnded; +/** is the body of the event toolchain-reported. */ +export type ToolchainReportedEvent = ToolchainReported; + +/** is the body of the event step-progress. */ +export type StepProgressEvent = StepProgress; + diff --git a/contracts/ts/seats/nodebuildnode/parts/build-step.js b/contracts/ts/seats/nodebuildnode/parts/build-step.js index 027da325..155a68be 100644 --- a/contracts/ts/seats/nodebuildnode/parts/build-step.js +++ b/contracts/ts/seats/nodebuildnode/parts/build-step.js @@ -13,6 +13,8 @@ export const verbs = { export const events = { stepEnded: "step-ended", + toolchainReported: "toolchain-reported", + stepProgress: "step-progress", }; export {}; From 2c1f5a40332720fc564612add61160c5c10e243b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:21:20 +0100 Subject: [PATCH 2/3] VW1: the view reads the modules from the catalog half of the trunk, by meshcatalogclient.Catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The view's process follows the trunk's catalog half (first view of the seat mesh-catalog's verb revision, then each catalog-revision of branch main) and hands each changed entry to its model in the form of the value it always kept; its tools read the newest trunk revision through the verb. The manifest invokes seat:mesh-catalog.revision and consumes mesh-catalog.catalog-revision instead of the indexer's modules read tool and views family. Builds: design 47 G §196, test 61 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../mesh-delivery-view-tools/catalogread.go | 83 +++++++++++++++++ .../cmd/mesh-delivery-view-tools/main.go | 2 +- .../mesh-delivery-view-tools/owners_test.go | 56 ++++++++--- .../mesh-delivery-view/integration_test.go | 92 +++++++++++++++---- .../cmd/mesh-delivery-view/main.go | 52 +++++++++-- .../internal/view/catalog.go | 58 ++++++++++++ .../mesh-delivery-view/internal/view/names.go | 1 - modules/mesh-delivery-view/manifest_test.go | 19 +++- modules/mesh-delivery-view/module.json | 4 +- 9 files changed, 321 insertions(+), 46 deletions(-) create mode 100644 modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/catalogread.go create mode 100644 modules/mesh-delivery-view/internal/view/catalog.go diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/catalogread.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/catalogread.go new file mode 100644 index 00000000..97803058 --- /dev/null +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/catalogread.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "sort" + "sync" + "time" + + "mesh-delivery-view/internal/view" +) + +// catalogWithin is how long a call waits for the seat mesh-catalog's first view before it says there is none. +var catalogWithin = 30 * time.Second + +// catalogRead is the modules the view's tools answer, read from the catalog half through the seat mesh-catalog's verb +// `revision` (design 47, Two graphs): the trunk's newest revision, asked once per call and in pages, each entry in the +// form of the value the tools always answered. With no view the read says so; it never answers no modules. +func catalogRead(ask func(key string, body any) (json.RawMessage, error)) view.StateRead { + var ( + once sync.Once + values map[string]json.RawMessage + rev uint64 + failed error + ) + load := func() { + ctx, cancel := context.WithTimeout(context.Background(), catalogWithin) + defer cancel() + c, err := meshcatalogclient.NewCatalog(ask, "main") + if err != nil { + failed = err + return + } + if err := c.Start(ctx); err != nil { + failed = fmt.Errorf("the catalog half has no first view yet (%s): %w", meshcatalogclient.RevisionVerb, err) + return + } + half, ok := c.View() + if !ok { + failed = fmt.Errorf("the catalog half has no first view yet (%s)", meshcatalogclient.RevisionVerb) + return + } + rev, values = uint64(half.Revision.Revision), map[string]json.RawMessage{} + for module, e := range half.Entries { + raw, _ := json.Marshal(e) + v, err := view.ModuleValue(raw) + if err != nil { + failed = fmt.Errorf("the catalog entry of %s: %w", module, err) + return + } + values[module] = v + } + } + read := func() (map[string]json.RawMessage, uint64, error) { + once.Do(load) + return values, rev, failed + } + return view.StateRead{ + Keys: func() ([]string, error) { + v, _, err := read() + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + return keys, err + }, + Get: func(key string) (json.RawMessage, error) { + v, _, err := read() + return v[key], err + }, + GetEntry: func(key string) (json.RawMessage, uint64, error) { + v, r, err := read() + return v[key], r, err + }, + All: func() (map[string]json.RawMessage, error) { + v, _, err := read() + return v, err + }, + } +} diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 1a8f828a..9baa4b52 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -396,7 +396,7 @@ func ownersRead(ask func(key string, body any) (json.RawMessage, error)) view.Re // Each owner's read tool as the view's invokes names it. indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, view.IndexerModule+"."+tool) } assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, view.AssignerModule+"."+tool) } - return view.Reader{State: indexer("indexer_read_modules"), CandidatesState: indexer("indexer_read_candidates"), + return view.Reader{State: catalogRead(ask), CandidatesState: indexer("indexer_read_candidates"), WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"), HeardState: indexer("indexer_read_heard"), PredefinedState: indexer("indexer_read_predefined"), diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go index 80ad8589..8864369f 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go @@ -3,12 +3,14 @@ package main import ( "encoding/json" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "mesh-delivery-view/internal/view" "sort" "strings" "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -51,7 +53,6 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { t.Fatal(err) } states := map[string]source{ - view.IndexerModule + ".modules": {"a": withFP2, "b": withNone}, view.IndexerModule + ".candidates": {"feat/x": cand, "fix": `{"v":1,"group":"fix","base":3,"heads":"h","pulls":[{"modules":["netcheck"]}]}`}, view.AssignerModule + ".wants": {"want.ace.zsh": `{"fp":"` + fp2A + `"}`, "want.novox.zsh": `{"fp":"` + fp2B + `"}`, "held." + keyB: `{"node":"ace","words":"failed"}`}, @@ -67,14 +68,12 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { const heard1 = `{"v":1,"subject":"s","sequence":41,"repository":"novox/mesh-catalog","commit":"9cdac09a"}` states[view.IndexerModule+".heard"] = source{"forge": heard1} histories := map[string]map[string][]views.Revision{ - view.IndexerModule + ".modules": {"a": {{Revision: 7, At: at, Op: "put", Value: json.RawMessage(withNone)}, - {Revision: 9, At: at.Add(time.Minute), Op: "put", Value: json.RawMessage(withFP2)}}}, view.IndexerModule + ".heard": {"forge": {{Revision: 3, At: at, Op: "put", Value: json.RawMessage(heard1)}}}, view.AssignerModule + ".wants": {"want.ace.zsh": {{Revision: 12, At: at, Op: "put", Value: json.RawMessage(`{"fp":"` + fp2A + `"}`)}}}, } // The direct reading, as the tools read the states before (the reads the manifest no longer has). st := func(name string) view.StateRead { return fake(states[name]).reader().State } - direct := view.Reader{State: st(view.IndexerModule + ".modules"), CandidatesState: st(view.IndexerModule + ".candidates"), + direct := view.Reader{CandidatesState: st(view.IndexerModule + ".candidates"), WantsState: st(view.AssignerModule + ".wants"), DifferencesState: st(view.AssignerModule + ".differences"), PredefinedState: st(view.IndexerModule + "." + view.IndexerPredefinedState), ConfigCandidatesState: st(view.IndexerModule + "." + view.IndexerConfigCandidatesState), @@ -89,6 +88,10 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { } asked := map[string]int{} ask := func(key string, body any) (json.RawMessage, error) { + if key == meshcatalogclient.RevisionVerb { + asked[key]++ + return json.Marshal(catalogAnswer) + } name, ok := tools[key] if !ok { return nil, fmt.Errorf("no tool %s", key) @@ -118,10 +121,6 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { tool string args map[string]any }{ - {view.ModuleReadTool, map[string]any{"module": "a"}}, - {view.ModuleReadTool, map[string]any{"module": "gone"}}, - {view.ModulesReadTool, map[string]any{}}, - {view.ModulesReadTool, map[string]any{"none2": true}}, {view.CandidateReadTool, map[string]any{"group": "feat/x"}}, {view.CandidatesReadTool, map[string]any{}}, {view.CandidatesReadTool, map[string]any{"module": "zsh"}}, @@ -143,12 +142,24 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { t.Errorf("%s %v through the owner's read tool = %s, %v; reading the state it was %s, %v", row.tool, row.args, gj, gerr, wj, werr) } } - // A key's history is asked of the owner's read tool and answered from its kept values (A2j finding 1, J4). - out, err := run(t, owned, view.ModuleHistoryTool, map[string]any{"module": "a"}) - if h, _ := out.(view.ModuleHistory); err != nil || len(h.Values) != 2 || h.Values[0].Revision != 9 || h.Values[1].FP2 != "" || !h.Values[1].At.Equal(at) { - t.Errorf("indexer_module_history a through the indexer's read tool = %+v, %v", out, err) + // The modules are the catalog half's, asked of the seat mesh-catalog's verb revision (W16.1): a module is its entry in + // the form of the value the tool always answered, the modules without a fingerprint say why, and the module's history + // is not a thing the catalog half keeps. + a, err := run(t, owned, view.ModuleReadTool, map[string]any{"module": "a"}) + if raw, _ := json.Marshal(a); err != nil || !strings.Contains(string(raw), `"fp2":"`+fp2A+`"`) || !strings.Contains(string(raw), `"commit":"c1"`) || !strings.Contains(string(raw), `"src":7`) { + t.Errorf("indexer_module a = %s, %v", raw, err) } - out, err = run(t, owned, view.HeardTool, map[string]any{}) + if _, err := run(t, owned, view.ModuleReadTool, map[string]any{"module": "gone"}); err == nil { + t.Errorf("indexer_module gone answered a module the catalog half does not hold") + } + all, err := run(t, owned, view.ModulesReadTool, map[string]any{"none2": true}) + if raw, _ := json.Marshal(all); err != nil || !strings.Contains(string(raw), `"b"`) || strings.Contains(string(raw), `"a"`) || !strings.Contains(string(raw), "builder's fingerprint not yet known") { + t.Errorf("indexer_modules none2 = %s, %v", raw, err) + } + if _, err := run(t, owned, view.ModuleHistoryTool, map[string]any{"module": "a"}); err == nil { + t.Errorf("indexer_module_history answered a history the catalog half does not keep") + } + out, err := run(t, owned, view.HeardTool, map[string]any{}) if rows, _ := out.(view.HeardAnswer); err != nil || len(rows.Heard) != 1 || rows.Heard[0].Revision != 3 || string(rows.Heard[0].Heard) != heard1 { t.Errorf("indexer_heard through the indexer's read tool = %+v, %v", out, err) } @@ -156,10 +167,29 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) { if revs, err := owned.WantsState.History("want.ace.zsh"); err != nil || len(revs) != 1 || !revs[0].At.Equal(at) || revs[0].Revision != 12 { t.Errorf("the want's history through the assigner's read tool = %+v, %v", revs, err) } + // With no view of the catalog half the modules are not read: the tools say so, never that no module is held. + catalogWithin = 20 * time.Millisecond + none := ownersRead(func(key string, body any) (json.RawMessage, error) { return nil, fmt.Errorf("the holder is away") }) + if out, err := run(t, none, view.ModulesReadTool, map[string]any{}); err == nil || !strings.Contains(err.Error(), "no first view") { + t.Errorf("indexer_modules with no view = %v, %v, want an error naming it", out, err) + } // Each owner state the tools read was asked of its owner's read tool. for tool, name := range tools { if name != view.IndexerModule+"."+graph.State && asked[tool] == 0 { t.Errorf("%s was never asked", tool) } } + if asked[meshcatalogclient.RevisionVerb] == 0 { + t.Errorf("%s was never asked", meshcatalogclient.RevisionVerb) + } +} + +// catalogAnswer is the seat mesh-catalog's newest trunk revision: a with a fingerprint and the source revision it moved at, +// b with none and the reason. +var catalogAnswer = meshcatalog.RevisionAnswer{ + Revision: meshcatalog.CatalogRevision{Revision: 4, Kind: meshcatalogclient.KindTrunk, Source: 7}, + Entries: []meshcatalog.CatalogEntry{ + {Module: "a", Repository: "novox/r", Commit: "c1", Path: ".", Manifest: map[string]any{"module": "a"}, Fp: fp2A, Inputs: &meshcatalog.EntryInputs{Src: 7}}, + {Module: "b", Repository: "novox/r", Commit: "c2", Path: "modules/b", Manifest: map[string]any{"module": "b"}, None: "builder's fingerprint not yet known"}, + }, } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go index 77ad3d76..14248181 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go @@ -12,6 +12,7 @@ import ( "path/filepath" "slices" "strings" + "sync" "testing" "time" @@ -47,6 +48,67 @@ func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc * } } +// catalogHolder is the seat mesh-catalog's holder as the view's process needs it (W16.1): its verb revision answering the +// trunk's newest revision whole, and its event catalog-revision said on the shared events stream at each change, which the +// view's durable consumer hears. +type catalogHolder struct { + t testing.TB + js jetstream.JetStream + mu sync.Mutex + // rev is the revision's number; entries are its entries by module. + rev int64 + entries map[string]meshcatalog.CatalogEntry + ctx context.Context +} + +func serveCatalog(t testing.TB, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) *catalogHolder { + t.Helper() + h := &catalogHolder{t: t, js: js, ctx: ctx, rev: 1, entries: map[string]meshcatalog.CatalogEntry{}} + events, err := js.Stream(ctx, process.EventsStream) + if err != nil { + t.Fatal(err) + } + info := events.CachedInfo().Config + if !slices.Contains(info.Subjects, "mesh.seat.*.event.>") { + info.Subjects = append(info.Subjects, "mesh.seat.*.event.>") + if _, err := js.UpdateStream(ctx, info); err != nil { + t.Fatal(err) + } + } + answer := func(m *nats.Msg) { + h.mu.Lock() + a := meshcatalog.RevisionAnswer{Revision: meshcatalog.CatalogRevision{Revision: h.rev, Kind: meshcatalogclient.KindTrunk}} + for _, e := range h.entries { + a.Entries = append(a.Entries, e) + } + h.mu.Unlock() + raw, _ := json.Marshal(map[string]any{"result": a, "node": "n0"}) + _ = m.Respond(raw) + } + for _, subject := range []string{"mesh.seat.mesh-catalog.call.revision.>", "mesh.seat.mesh-catalog.call.revision"} { + sub, err := nc.Subscribe(subject, answer) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sub.Unsubscribe() }) + } + return h +} + +// put is the holder's next trunk revision with the entry of one module changed, said as catalog-revision. +func (h *catalogHolder) put(e meshcatalog.CatalogEntry) { + h.t.Helper() + h.mu.Lock() + h.rev++ + h.entries[e.Module] = e + rev := h.rev + h.mu.Unlock() + body, _ := json.Marshal(meshcatalog.CatalogRevisionEvent{Kind: meshcatalogclient.KindTrunk, Branch: "main", Revision: rev, Changed: []string{e.Module}}) + if _, err := h.js.Publish(h.ctx, "mesh.seat.mesh-catalog.event."+meshcatalog.EventCatalogRevision, body); err != nil { + h.t.Fatal(err) + } +} + // TestProcessWholeSeesTheBuckets starts the program the way the node does (its entry, run, with its environment and a // broker file) against an embedded bus holding the three buckets, and asks its socket. func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8 @@ -126,6 +188,7 @@ func TestProcessWholeSeesTheBuckets(t *testing.T) { // 1134/AC8 runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -226,6 +289,7 @@ func TestProcessWholeShowsWhatTheWatchmanSays(t *testing.T) { // 1168/AC5 runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -293,6 +357,7 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124 runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + catalog := serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -306,26 +371,16 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124 if err != nil { t.Fatal(err) } - mods, err := js.KeyValue(ctx, view.ModulesBucket) - if err != nil { - t.Fatal(err) - } const head = "0123456789abcdef0123456789abcdef01234567" hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: head}}) cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{Group: "feat/x", Heads: hash, Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-delivery", Number: 12, Head: head, Modules: []string{"a"}}}}) if err != nil { t.Fatal(err) } - mod, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{None: "no tree", Repository: "novox/mesh-delivery", Commit: head}) - if err != nil { - t.Fatal(err) - } if _, err := cands.Put(ctx, "feat/x", cand); err != nil { t.Fatal(err) } - if _, err := mods.Put(ctx, "a", mod); err != nil { - t.Fatal(err) - } + catalog.put(meshcatalog.CatalogEntry{Module: "a", None: "no tree", Repository: "novox/mesh-delivery", Commit: head, Manifest: map[string]any{"module": "a"}}) var last view.Snapshot for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) { if s, err := view.Ask(socket); err == nil { @@ -380,6 +435,7 @@ func TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes(t *testing.T) { // runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -471,6 +527,7 @@ func TestProcessWholeListsEveryDifference(t *testing.T) { // 1297/B8 AC runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -569,6 +626,7 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129 runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -721,6 +779,7 @@ func TestProcessWholeShowsTheBacklogOfBuildAsks(t *testing.T) { // 1887/A8a AC runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) @@ -768,7 +827,7 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) { t.Fatal(err) } kvs := map[string]jetstream.KeyValue{} - for _, name := range []string{view.SourceBucket, view.ModulesBucket, view.AssignerWantsBucket, view.HaveBucket} { + for _, name := range []string{view.SourceBucket, view.AssignerWantsBucket, view.HaveBucket} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 5}) if err != nil { t.Fatal(err) @@ -789,20 +848,16 @@ func TestProcessWholeSaysTheSourceRevisionIsDelivered(t *testing.T) { runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + catalog := serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) fp := "src2:" + strings.Repeat("2", 64) - mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: strings.Repeat("a", 64), Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath}) want, _ := meshwantsclient.EncodeWant(meshwantsclient.Want{Fp: fp, Composed: "sha256:" + strings.Repeat("c", 64)}) failed, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthFailed}) good, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) src, _ := meshdeliveryclient.EncodeSource(meshdelivery.SourceByBranchValue{Group: "feat/x", Heads: map[string]string{"novox/web": strings.Repeat("1", 40)}}) - for bucket, kv := range map[string][]byte{view.ModulesBucket: mod} { - if _, err := kvs[bucket].Put(ctx, "web", kv); err != nil { - t.Fatal(err) - } - } + catalog.put(meshcatalog.CatalogEntry{Module: "web", Fp: fp, Manifest: map[string]any{"module": "a"}, Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: meshcatalogclient.RootPath}) if _, err := kvs[view.AssignerWantsBucket].Put(ctx, "want.ace.web", want); err != nil { t.Fatal(err) } @@ -886,6 +941,7 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) { runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) serveOwners(t, ctx, js, nc) + serveCatalog(t, ctx, js, nc) go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() t.Cleanup(func() { stop(); <-done }) time.Sleep(2 * time.Second) // the view has read what stood: nothing diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go index 471875c1..ecd9dcd5 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "io" "log" "mesh-delivery-view/internal/view" @@ -126,15 +127,21 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } } open := buckets{js: js, reader: reader} - for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.ModulesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} { + for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} { go follow(ctx, open, bucket, false, model, logger) } - // The modules' and the source's history give each fingerprint's first commit and each head's source revision (ADR 0556). - // The modules' picture holds no history: from its first view on, each change it takes is the history (finding of DLV-M4). - for _, bucket := range []string{view.ModulesBucket, view.SourceBucket} { - go follow(ctx, open, bucket, true, model, logger) + // The source's history gives each head's source revision (ADR 0556); the modules' history is each revision of the + // catalog half the view takes, in its order (below). + go follow(ctx, open, view.SourceBucket, true, model, logger) + statusBucket := process.AwaitBucket(ctx, js, conn, firstViewsBucket, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf) + // The modules are the catalog half's, adopted whole by the seat's client: its first view, its status, its hand asks and + // its event catalog-revision (on the module's durable consumer) are the reader's. + catalog, _, err := meshcatalogclient.WireCatalogProcess(reader, calls.Call, "main", catalogChanged(model, logger)) + if err != nil { + return err } - reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "view_first_views reads nothing and view_first_view_again is not heard", logger.Printf)) + reader.Start(ctx, js, cred, statusBucket) + catalogHeld(ctx, catalog, model) select { case <-ctx.Done(): case err := <-webUp: @@ -189,7 +196,6 @@ var ownersFollowed = []struct{ owner, state, tool string }{ {view.AssignerModule, "wants", view.AssignerModule + ".assigner_read_wants"}, {view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"}, {view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"}, - {view.IndexerModule, "modules", view.IndexerModule + ".indexer_read_modules"}, {view.IndexerModule, "taken", view.IndexerModule + ".indexer_read_taken"}, } @@ -277,3 +283,35 @@ func emitEvent(ctx context.Context, js jetstream.JetStream, cred process.Account _, err = js.PublishMsg(pub, msg, jetstream.WithMsgID(id)) return err } + +// catalogChanged hands one module of a revision the view took to the model, in the form of its value; the modules' +// history is these changes in the order of the revisions. +func catalogChanged(model *view.Model, logger *log.Logger) func(string, json.RawMessage, bool, time.Time) { + return func(module string, entry json.RawMessage, gone bool, at time.Time) { + e := view.Entry{Bucket: view.ModulesBucket, Key: module, At: at, Deleted: gone} + if !gone { + v, err := view.ModuleValue(entry) + if err != nil { + logger.Printf("the catalog entry of %s is not taken: %v", module, err) + return + } + e.Value = v + } + model.Apply(e) + e.History = true + model.Apply(e) + } +} + +// catalogHeld makes the model's pictures of the modules initial once the catalog half's first view is held: the process +// starts at once and waits for the seat's holder, saying so. +func catalogHeld(ctx context.Context, cat *meshcatalogclient.Catalog, model *view.Model) { + go func() { + select { + case <-cat.Follower.Ready(): + model.Initial(view.ModulesBucket) + model.Initial(view.ReachHistory(view.ModulesBucket)) + case <-ctx.Done(): + } + }() +} diff --git a/modules/mesh-delivery-view/internal/view/catalog.go b/modules/mesh-delivery-view/internal/view/catalog.go new file mode 100644 index 00000000..385de245 --- /dev/null +++ b/modules/mesh-delivery-view/internal/view/catalog.go @@ -0,0 +1,58 @@ +package view + +// The view reads the catalog half of the delivery through the seat mesh-catalog (design 47, Two graphs; ADR 0656 §6): +// a first view of its verb `revision`, then each trunk revision its event `catalog-revision` says, never the indexer's +// state `modules`. The model keeps the picture it always kept (one value per module, its history for the reach), so each +// entry of a revision is handed to it in the form of that value. + +import ( + "encoding/json" + "fmt" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// CatalogSeat is the seat whose verb `revision` and event `catalog-revision` the view reads the modules from, as its +// manifest's `invokes` and `consumes` name them. +const CatalogSeat = meshcatalog.Seat + +// ModulesBucket names the model's picture of the catalog half's modules (and its history): the model's label, no +// bucket of anybody's. +const ModulesBucket = CatalogSeat + "_modules" + +// ModuleValue is the value of one module of the model, as the model decodes it, made from the catalog entry (its JSON): +// the fingerprint is the entry's `fp` (the only form there is), the source revisions are the entry's inputs'. A manifest +// held for unknown fields has no verdict here: the model reads the fingerprint and the commit. +func ModuleValue(entry json.RawMessage) (json.RawMessage, error) { + var e meshcatalog.CatalogEntry + if err := json.Unmarshal(entry, &e); err != nil || e.Module == "" { + return nil, fmt.Errorf("a catalog entry does not read: %v", err) + } + v := meshcatalog.ModulesByModuleValue{Commit: e.Commit, Fp2: e.Fp, None2: e.None, Path: e.Path, Repository: e.Repository, Packages: e.Packages} + if len(e.Manifest) > 0 { + // The entry holds its manifest whole; the model keeps the digest of its bytes, as the value it always kept does. + raw, _ := json.Marshal(e.Manifest) + v.Manifest = meshcatalogclient.ManifestDigest(raw) + } + if e.Verdict == meshcatalogclient.VerdictAccepted || e.Verdict == meshcatalogclient.VerdictRefused { + v.Verdict = e.Verdict + } + if in := e.Inputs; in != nil { + v.Src, v.FpSrc, v.Toolchains, v.Paths, v.Warnings = in.Src, in.FpSrc, in.Toolchains, in.Paths, in.Warnings + if f := in.From; f != nil && v.Manifest != "" { + v.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Bases: f.Bases, Builder: f.Builder, Contexts: f.Contexts, + ImportForm: f.ImportForm, Imports: f.Imports, Locks: f.Locks, Packages: f.Packages, Paths: f.Paths, Tree: f.Tree} + } + if v.Verdict == meshcatalogclient.VerdictRefused { + v.Problems = in.Problems + } + } + if v.Verdict == meshcatalogclient.VerdictRefused && len(v.Problems) == 0 { + v.Verdict = "" + } + if v.Verdict != meshcatalogclient.VerdictRefused { + v.Warnings = nil + } + return meshcatalogclient.EncodeModule(v) +} diff --git a/modules/mesh-delivery-view/internal/view/names.go b/modules/mesh-delivery-view/internal/view/names.go index 4a2d62f1..24e69c85 100644 --- a/modules/mesh-delivery-view/internal/view/names.go +++ b/modules/mesh-delivery-view/internal/view/names.go @@ -24,7 +24,6 @@ const ( EngineModule = "node-engine" // The indexer's states the view reads. - ModulesBucket = IndexerModule + "_modules" CandidatesBucket = IndexerModule + "_candidates" TakenBucket = IndexerModule + "_taken" PackagesState = "packages" diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index b35cf6f8..693fe5d5 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -3,6 +3,7 @@ package manifest import ( "bytes" "encoding/json" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "io/fs" "mesh-delivery-view/internal/view" "os" @@ -119,8 +120,9 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 // follows then heard on their views families (ADR 0615 §1, §5; DLV-M4). if want := []string{"mesh-delivery-assigner.assigner_read_asks", "mesh-delivery-assigner.assigner_read_differences", "mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_config_candidates", - "mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending", "mesh-delivery-indexer.indexer_read_modules", + "mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken", + "seat:mesh-catalog.revision", // ADR 0663: the modules are the catalog half's, read through the seat's verb revision, never the indexer's "seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools) "seat:node-engine.manifest-reads"}; !reflect.DeepEqual(m.Invokes, want) { // ADR 0618 §3: node_manifest_reads asks the node-engine seat's verb of every node; 1585/T24: view_tiers asks indexer_pending, a verb edge (ADR 0614 §2) t.Errorf("invokes = %v, want %v", m.Invokes, want) @@ -133,7 +135,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 t.Errorf("version = %q, want 0.1", m.Version) } if want := []string{"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants", - "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) { + "mesh-catalog.catalog-revision", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } // The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves. @@ -180,7 +182,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if !slices.Contains(m.Reads, view.SignersRead) { t.Errorf("the view reads %s, its manifest's reads are %v", view.SignersRead, m.Reads) } - followed := map[string]string{view.ModulesBucket: view.IndexerModule + ".views.modules", view.CandidatesBucket: view.IndexerModule + ".views.candidates", + followed := map[string]string{view.CandidatesBucket: view.IndexerModule + ".views.candidates", view.TakenBucket: view.IndexerModule + ".views.taken", view.AssignmentsBucket: view.AssignerModule + ".views.assignments", view.AssignerWantsBucket: view.AssignerModule + ".views.wants", view.AssignerAsksBucket: view.AssignerModule + ".views.asks"} for bucket, family := range followed { @@ -191,11 +193,20 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes) } } - for _, tool := range []string{view.IndexerModule + ".indexer_read_modules", view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants"} { + for _, tool := range []string{meshcatalogclient.RevisionVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants"} { if !slices.Contains(m.Invokes, tool) { t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes) } } + // The modules are the catalog half's: the seat's verb is invoked, its event consumed, and the indexer's state modules is neither. + if !slices.Contains(m.Consumes, view.CatalogSeat+"."+meshcatalog.EventCatalogRevision) { + t.Errorf("the view hears the seat's %s, its manifest consumes %v", meshcatalog.EventCatalogRevision, m.Consumes) + } + for _, name := range append(slices.Clone(m.Invokes), m.Consumes...) { + if strings.Contains(name, "indexer_read_modules") || strings.Contains(name, "views.modules") { + t.Errorf("the manifest names %s: the modules are the catalog half's (ADR 0663)", name) + } + } if viewstate.ViewModule != m.Module || viewstate.ViewAnswersBucket != m.Module+"_answers" || viewstate.ReachBucket != m.Module+"_reach" { t.Errorf("the view names itself %s (%s, %s), its manifest %s", viewstate.ViewModule, viewstate.ViewAnswersBucket, viewstate.ReachBucket, m.Module) } diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index a8544e42..9e727992 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -46,8 +46,8 @@ "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants", + "mesh-catalog.catalog-revision", "mesh-delivery-indexer.views.candidates", - "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.taken" ], "streams": [ @@ -68,10 +68,10 @@ "mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending", - "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken", + "seat:mesh-catalog.revision", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads" ], From e1499f0617a321c18ef93907a93af88309daff49 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:21:56 +0100 Subject: [PATCH 3/3] CT4: a wait names its clock and its kind: clock-from, step of the acting steps, a put's fingerprint, a delivery wait's machines; regenerated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-wants' Wait gains clock-from, the step enum of section 203 with put, delivery, kept-build and backup, fingerprint and machines, and requires step, module and since only (a put's wait has no part); the runner's wait body gains clock-from. The runner's `step` already names pipeline steps: see the report. Builds: design 47 G §498, §266, §269 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../go/seats/meshpipelinerunner/contract.go | 2 ++ contracts/go/seats/meshwants/contract.go | 31 +++++++++++++--- contracts/go/seats/meshwants/contract_test.go | 4 +++ .../seats/mesh-pipeline-runner/seat.json | 5 +++ .../seats/mesh-wants/fixtures/events.json | 32 +++++++++++++++++ contracts/seats/mesh-wants/seat.json | 35 +++++++++++++++++-- .../ts/seats/meshpipelinerunner/index.d.ts | 2 ++ contracts/ts/seats/meshwants/index.d.ts | 13 +++++-- 8 files changed, 114 insertions(+), 10 deletions(-) diff --git a/contracts/go/seats/meshpipelinerunner/contract.go b/contracts/go/seats/meshpipelinerunner/contract.go index 39817255..7c17dd91 100644 --- a/contracts/go/seats/meshpipelinerunner/contract.go +++ b/contracts/go/seats/meshpipelinerunner/contract.go @@ -675,6 +675,8 @@ type Wait struct { Artifact string `json:"artifact,omitempty"` // the watched branch, or the branch of a run on request Branch string `json:"branch"` + // the time the bound counts from (design 47 G section 266, section 269, section 498) + ClockFrom *time.Time `json:"clock-from,omitempty"` // the commit the module's entry was last changed at Commit string `json:"commit,omitempty"` // the module's build-input fingerprint at the commit diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go index 9b9f4754..ad44c028 100644 --- a/contracts/go/seats/meshwants/contract.go +++ b/contracts/go/seats/meshwants/contract.go @@ -659,20 +659,43 @@ type Wait struct { Bound string `json:"bound,omitempty"` // on a release, who released it and why By string `json:"by,omitempty"` - Fp string `json:"fp,omitempty"` + // the time the bound counts from (design 47 G section 266, section 269, section 498) + ClockFrom *time.Time `json:"clock-from,omitempty"` + // a put's wait: the fingerprint of the build put + Fingerprint string `json:"fingerprint,omitempty"` + Fp string `json:"fp,omitempty"` // on wait-ended: ran or released How string `json:"how,omitempty"` + // a dependent's delivery wait: the machines the dependency's version has yet to reach + Machines []string `json:"machines,omitempty"` // the entry that waits Module string `json:"module"` Node string `json:"node,omitempty"` // the acting part waited for - Part string `json:"part"` + Part string `json:"part,omitempty"` Since time.Time `json:"since"` - Step string `json:"step"` + // which wait: an acting step of section 203 or put, delivery, kept-build, backup (design 47 G section 498) + Step string `json:"step"` // its new version's fingerprint - Version string `json:"version"` + Version string `json:"version,omitempty"` } +// The values of Wait's step. +const ( + WaitStepTakeIn = "take-in" + WaitStepValidate = "validate" + WaitStepCheck = "check" + WaitStepBuild = "build" + WaitStepCompose = "compose" + WaitStepGrant = "grant" + WaitStepPlace = "place" + WaitStepPipeline = "pipeline" + WaitStepPut = "put" + WaitStepDelivery = "delivery" + WaitStepKeptBuild = "kept-build" + WaitStepBackup = "backup" +) + // UpgradeInput is what upgrade takes. type UpgradeInput struct { // "true": each node's own composition, by its digest, and the assigner's findings diff --git a/contracts/go/seats/meshwants/contract_test.go b/contracts/go/seats/meshwants/contract_test.go index 6a6825e6..b0624c3e 100644 --- a/contracts/go/seats/meshwants/contract_test.go +++ b/contracts/go/seats/meshwants/contract_test.go @@ -45,6 +45,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v VersionDeliveredEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.version-delivered$")}, {name: "a composition step's progress", subject: "mesh.seat.mesh-wants.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"assigner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"build\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"assigner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"build\"}", decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.step-progress\\.[^.*> ]+$")}, + {name: "a put without hold waits, counted from its start", subject: "mesh.seat.mesh-wants.event.wait-held", given: "{\"bound\":\"30m\",\"clock-from\":\"2026-11-11T10:00:00Z\",\"fingerprint\":\"fp-0123\",\"module\":\"nats\",\"node\":\"ace\",\"since\":\"2026-11-11T10:00:00Z\",\"step\":\"put\"}", wire: "{\"bound\":\"30m\",\"clock-from\":\"2026-11-11T10:00:00Z\",\"fingerprint\":\"fp-0123\",\"module\":\"nats\",\"node\":\"ace\",\"since\":\"2026-11-11T10:00:00Z\",\"step\":\"put\"}", + decode: func(b []byte) (any, error) { var v WaitHeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.wait-held$")}, + {name: "a dependent's delivery waits for a dependency's version", subject: "mesh.seat.mesh-wants.event.wait-held", given: "{\"bound\":\"60m\",\"clock-from\":\"2026-11-11T09:58:00Z\",\"machines\":[\"ace\",\"g14\"],\"module\":\"nats\",\"part\":\"mesh-sdk\",\"since\":\"2026-11-11T10:00:00Z\",\"step\":\"delivery\",\"version\":\"0.1.4\"}", wire: "{\"bound\":\"60m\",\"clock-from\":\"2026-11-11T09:58:00Z\",\"machines\":[\"ace\",\"g14\"],\"module\":\"nats\",\"part\":\"mesh-sdk\",\"since\":\"2026-11-11T10:00:00Z\",\"step\":\"delivery\",\"version\":\"0.1.4\"}", + decode: func(b []byte) (any, error) { var v WaitHeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-wants\\.event\\.wait-held$")}, {name: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", decode: func(b []byte) (any, error) { var v WantsByWantNodeModuleValue diff --git a/contracts/seats/mesh-pipeline-runner/seat.json b/contracts/seats/mesh-pipeline-runner/seat.json index 5d972333..29d7b274 100644 --- a/contracts/seats/mesh-pipeline-runner/seat.json +++ b/contracts/seats/mesh-pipeline-runner/seat.json @@ -1483,6 +1483,11 @@ "how": { "type": "string", "description": "for its end, how it ended: answered, ran, released, superseded" + }, + "clock-from": { + "type": "string", + "format": "date-time", + "description": "the time the bound counts from (design 47 G section 266, section 269, section 498)" } }, "description": "a step's wait, held and ended; a wait on a person is said as waiting on that person, never a stall (ADR 0283)", diff --git a/contracts/seats/mesh-wants/fixtures/events.json b/contracts/seats/mesh-wants/fixtures/events.json index 7c00e058..550c6c38 100644 --- a/contracts/seats/mesh-wants/fixtures/events.json +++ b/contracts/seats/mesh-wants/fixtures/events.json @@ -177,5 +177,37 @@ "phase": "build", "at": "2026-11-11T10:01:00Z" } + }, + { + "name": "a put without hold waits, counted from its start", + "of": "event:wait-held", + "subject": "mesh.seat.mesh-wants.event.wait-held", + "given": { + "step": "put", + "node": "ace", + "module": "nats", + "fingerprint": "fp-0123", + "since": "2026-11-11T10:00:00Z", + "clock-from": "2026-11-11T10:00:00Z", + "bound": "30m" + } + }, + { + "name": "a dependent's delivery waits for a dependency's version", + "of": "event:wait-held", + "subject": "mesh.seat.mesh-wants.event.wait-held", + "given": { + "step": "delivery", + "part": "mesh-sdk", + "version": "0.1.4", + "machines": [ + "ace", + "g14" + ], + "module": "nats", + "since": "2026-11-11T10:00:00Z", + "clock-from": "2026-11-11T09:58:00Z", + "bound": "60m" + } } ] diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json index aca8f3b9..6e41ad9f 100644 --- a/contracts/seats/mesh-wants/seat.json +++ b/contracts/seats/mesh-wants/seat.json @@ -1025,7 +1025,22 @@ "description": "a step's wait the assigner holds: placing, wanting or composing until an acting part's new version runs", "properties": { "step": { - "type": "string" + "type": "string", + "enum": [ + "take-in", + "validate", + "check", + "build", + "compose", + "grant", + "place", + "pipeline", + "put", + "delivery", + "kept-build", + "backup" + ], + "description": "which wait: an acting step of section 203 or put, delivery, kept-build, backup (design 47 G section 498)" }, "part": { "type": "string", @@ -1060,12 +1075,26 @@ "by": { "type": "string", "description": "on a release, who released it and why" + }, + "clock-from": { + "type": "string", + "format": "date-time", + "description": "the time the bound counts from (design 47 G section 266, section 269, section 498)" + }, + "fingerprint": { + "type": "string", + "description": "a put's wait: the fingerprint of the build put" + }, + "machines": { + "type": "array", + "items": { + "type": "string" + }, + "description": "a dependent's delivery wait: the machines the dependency's version has yet to reach" } }, "required": [ "step", - "part", - "version", "module", "since" ] diff --git a/contracts/ts/seats/meshpipelinerunner/index.d.ts b/contracts/ts/seats/meshpipelinerunner/index.d.ts index cf1d8263..abcd517c 100644 --- a/contracts/ts/seats/meshpipelinerunner/index.d.ts +++ b/contracts/ts/seats/meshpipelinerunner/index.d.ts @@ -364,6 +364,8 @@ export interface Wait { "artifact"?: string; /** the watched branch, or the branch of a run on request */ "branch": string; + /** the time the bound counts from (design 47 G section 266, section 269, section 498) */ + "clock-from"?: string; /** the commit the module's entry was last changed at */ "commit"?: string; /** the module's build-input fingerprint at the commit */ diff --git a/contracts/ts/seats/meshwants/index.d.ts b/contracts/ts/seats/meshwants/index.d.ts index a2760e6c..91a28584 100644 --- a/contracts/ts/seats/meshwants/index.d.ts +++ b/contracts/ts/seats/meshwants/index.d.ts @@ -311,18 +311,25 @@ export interface Wait { "bound"?: string; /** on a release, who released it and why */ "by"?: string; + /** the time the bound counts from (design 47 G section 266, section 269, section 498) */ + "clock-from"?: string; + /** a put's wait: the fingerprint of the build put */ + "fingerprint"?: string; "fp"?: string; /** on wait-ended: ran or released */ "how"?: string; + /** a dependent's delivery wait: the machines the dependency's version has yet to reach */ + "machines"?: string[]; /** the entry that waits */ "module": string; "node"?: string; /** the acting part waited for */ - "part": string; + "part"?: string; "since": string; - "step": string; + /** which wait: an acting step of section 203 or put, delivery, kept-build, backup (design 47 G section 498) */ + "step": "take-in" | "validate" | "check" | "build" | "compose" | "grant" | "place" | "pipeline" | "put" | "delivery" | "kept-build" | "backup"; /** its new version's fingerprint */ - "version": string; + "version"?: string; } /** is what upgrade takes. */