From 247c422fbfb6a3c9b555d23cf4a73133b1648e27 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 04:56:25 +0100 Subject: [PATCH] BG9, BN9, BI3: the build seats' steps with drop writes the hand-act log first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A drop of a kept detected-steps key records the act with the caller the bus proved, then deletes the key; a log that refuses deletes nothing and is answered error; a drop without why is refused; fetch is asked unless drop. Builds: design 47 G §463, §480 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../mesh-build-go/buildstep/dropdetected.go | 35 ++++++++++++++++++ .../cmd/mesh-build-go/handact.go | 13 +++++++ .../mesh-build-go/cmd/mesh-build-go/main.go | 2 + .../mesh-build-go/cmd/mesh-build-go/module.go | 4 ++ .../mesh-build-go/cmd/mesh-build-go/steps.go | 33 +++++++++++++++-- .../cmd/mesh-build-go/steps_test.go | 32 ++++++++++++++++ modules/mesh-build-go/go.mod | 2 + modules/mesh-build-go/module.json | 1 + .../cmd/mesh-build-image/asks.go | 2 +- .../cmd/mesh-build-image/compose_test.go | 2 +- .../cmd/mesh-build-image/handact.go | 13 +++++++ .../cmd/mesh-build-image/main.go | 1 + .../cmd/mesh-build-image/module.go | 2 + .../cmd/mesh-build-image/steps.go | 30 +++++++++++++-- .../cmd/mesh-build-image/steps_test.go | 26 ++++++++++++- modules/mesh-build-image/go.mod | 2 + modules/mesh-build-image/module.json | 1 + .../cmd/mesh-build-node/handact.go | 13 +++++++ .../cmd/mesh-build-node/main.go | 2 + .../cmd/mesh-build-node/module.go | 4 ++ .../cmd/mesh-build-node/steps.go | 37 +++++++++++++++++-- .../cmd/mesh-build-node/steps_test.go | 26 +++++++++++++ modules/mesh-build-node/go.mod | 2 + modules/mesh-build-node/module.json | 1 + 24 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 modules/mesh-build-go/buildstep/dropdetected.go create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/handact.go create mode 100644 modules/mesh-build-image/cmd/mesh-build-image/handact.go create mode 100644 modules/mesh-build-node/cmd/mesh-build-node/handact.go diff --git a/modules/mesh-build-go/buildstep/dropdetected.go b/modules/mesh-build-go/buildstep/dropdetected.go new file mode 100644 index 000000000..986fe78a2 --- /dev/null +++ b/modules/mesh-build-go/buildstep/dropdetected.go @@ -0,0 +1,35 @@ +package buildstep + +import ( + "errors" + "fmt" + "strings" +) + +// DropDetected is the drop of the build seats' `steps` (design 47 G §463, §480): a person's word deletes the kept key of +// an artifact's detected steps. The act is written in the hand-act log first, with the caller the bus proved; a drop +// whose log write fails deletes nothing and is answered by the text it returns, which names the log. A drop with no +// `why`, or whose call the runtime proved no caller for, is refused (an error, nothing recorded). +// +// record writes the act in the hand-act log of the seat mesh-hand-acts; forget deletes the key (nil where the seat keeps +// none, as an image's). +func DropDetected(artifact, why, caller, key string, record func(what, why string) error, forget func(key string) error) (string, error) { + switch { + case strings.TrimSpace(why) == "": + return "", errors.New("steps: a drop says why") + case caller == "" || caller == "unproven": + return "", errors.New("steps: a drop names who drops it, and the runtime proved no caller") + } + if record == nil { + return "the hand-act log is not served here: nothing was deleted", nil + } + if err := record(fmt.Sprintf("dropped the detected steps of %s (by %s)", artifact, caller), why); err != nil { + return fmt.Sprintf("the hand-act log refused the drop of %s, so nothing was deleted: %v", artifact, err), nil + } + if forget != nil { + if err := forget(key); err != nil { + return fmt.Sprintf("the drop of %s was recorded in the hand-act log and the key could not be deleted: %v", artifact, err), nil + } + } + return "", nil +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/handact.go b/modules/mesh-build-go/cmd/mesh-build-go/handact.go new file mode 100644 index 000000000..2903d3b6a --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/handact.go @@ -0,0 +1,13 @@ +package main + +import ( + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// handActRecorder writes an act by hand in the hand-act log of the seat mesh-hand-acts (design 47 G §480); a log that +// does not take it is an error, never a quiet success. +func handActRecorder(what, why string) error { + _, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "build-steps-drop", Verb: "steps drop"}) + return err +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/main.go b/modules/mesh-build-go/cmd/mesh-build-go/main.go index 8817fc799..edbf844b3 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -44,6 +44,8 @@ func main() { Steps: stdio.State(seat.StateSteps).Put, Detected: stdio.State(seat.StateDetectedSteps).Put, ReadDetected: readOf(stdio.State(seat.StateDetectedSteps)).Get, + ForgetDetected: stdio.State(seat.StateDetectedSteps).Delete, + RecordAct: handActRecorder, ReadSteps: readOf(stdio.State(seat.StateSteps)), Pauses: stdio.State(sdkstep.PausesState).Put, ReadPauses: readOf(stdio.State(sdkstep.PausesState)), diff --git a/modules/mesh-build-go/cmd/mesh-build-go/module.go b/modules/mesh-build-go/cmd/mesh-build-go/module.go index 26632aed3..5f0f36909 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -37,6 +37,10 @@ type Module struct { // answer of the verb steps, one key per artifact. Detected buildstep.Put ReadDetected func(key string) (json.RawMessage, error) + // ForgetDetected deletes one key of detected-steps; RecordAct writes an act in the hand-act log of the seat + // mesh-hand-acts (§463, §480): a drop writes it first. + ForgetDetected func(key string) error + RecordAct func(what, why string) error // Pauses is the Put of the pauses state, which keeps the node's pause key without an age; ReadPauses reads it back. Pauses buildstep.Put ReadPauses StateRead diff --git a/modules/mesh-build-go/cmd/mesh-build-go/steps.go b/modules/mesh-build-go/cmd/mesh-build-go/steps.go index 7745cc695..7d9ddde4a 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/steps.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/steps.go @@ -130,6 +130,9 @@ type StepsAnswer struct { Fingerprint string `json:"fingerprint,omitempty"` Steps []DetectedStep `json:"steps"` Kept bool `json:"kept,omitempty"` + // Dropped is the artifact whose key a drop deleted; Error why a drop deleted nothing, naming the hand-act log. + Dropped string `json:"dropped,omitempty"` + Error string `json:"error,omitempty"` } // stepsTool is the seat's verb `steps`: it fetches the artifact's commit into a workspace of its own, only the tree (no @@ -138,26 +141,31 @@ func (m *Module) stepsTool() stdio.Tool { return stdio.Tool{Name: Seat + "." + seat.VerbSteps, Description: "Which pipeline steps a Go artifact can run, found by Go's convention: lint always, unit tests where a package holds a test file, " + "integration tests where one holds a test file built with the tag integration. A level with no test is not answered.", - Input: map[string]any{"type": "object", "required": []string{"module", "artifact", "fetch"}, "properties": map[string]any{ + Input: map[string]any{"type": "object", "required": []string{"module", "artifact"}, "properties": map[string]any{ "module": map[string]any{"type": "string"}, "artifact": map[string]any{"type": "string", "description": "the artifact reference, :"}, "fingerprint": map[string]any{"type": "string"}, "fetch": map[string]any{"type": "object", "description": "what the fetch tool is asked: the repository, the full commit and the module's directory"}, "again": map[string]any{"type": "boolean", "description": "find them anew, not from the kept answer"}, + "drop": map[string]any{"type": "boolean", "description": "delete the artifact's kept key instead of detecting; needs why (written in the hand-act log first)"}, + "why": map[string]any{"type": "string", "description": "with drop, why: a non-empty text"}, }}, Run: m.Short(m.steps)} } func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) { str := func(k string) string { s, _ := args[k].(string); return s } + if str("artifact") == "" || str("module") == "" { + return nil, fmt.Errorf("steps: the module and the artifact are asked") + } + if drop, _ := args["drop"].(bool); drop { + return m.dropDetected(args, stdio.CallerOf(args)) + } fetch, _ := args["fetch"].(map[string]any) req, err := sdkstep.ParseFetchRequest(fetch) if err != nil { return nil, err } - if str("artifact") == "" || str("module") == "" { - return nil, fmt.Errorf("steps: the module and the artifact are asked") - } artifact, fp := str("artifact"), str("fingerprint") key := seat.DetectedStepsByModuleArtifactKey(str("module"), artifactName(artifact)) // The kept answer is the artifact's newest fingerprint's: used for the same fingerprint, replaced by a newer one @@ -181,6 +189,23 @@ func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) { return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: steps}, nil } +// dropDetected is `steps` with drop (§463, §480): the act is written in the hand-act log with the caller the bus proved, +// then the kept key is deleted; a log that refuses leaves the key. +func (m *Module) dropDetected(args map[string]any, caller string) (any, error) { + artifact, _ := args["artifact"].(string) + why, _ := args["why"].(string) + module, _ := args["module"].(string) + key := seat.DetectedStepsByModuleArtifactKey(module, artifactName(artifact)) + failed, err := buildstep.DropDetected(artifact, why, caller, key, m.RecordAct, m.ForgetDetected) + if err != nil { + return nil, err + } + if failed != "" { + return StepsAnswer{Artifact: artifact, Error: failed}, nil + } + return StepsAnswer{Artifact: artifact, Dropped: artifact}, nil +} + // artifactName is the artifact of an artifact reference :. func artifactName(ref string) string { if _, name, ok := strings.Cut(ref, ":"); ok { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/steps_test.go b/modules/mesh-build-go/cmd/mesh-build-go/steps_test.go index b60021fff..3113c4efb 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/steps_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/steps_test.go @@ -3,8 +3,10 @@ package main import ( "context" "encoding/json" + "errors" "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "reflect" + "strings" "testing" ) @@ -87,3 +89,33 @@ func TestTheStepsVerbKeepsItsAnswerByFingerprint(t *testing.T) { t.Fatalf("newer fingerprint: %+v", a) } } + +// Design 47 G §463, §480 (BG9): steps with drop writes the hand-act log first, then deletes the key; a log that +// refuses deletes nothing; a drop without why, or with no caller the bus proved, is refused; fetch is not asked. +func TestTheStepsDropWritesTheHandActLogFirst(t *testing.T) { + var order []string + kept := map[string]bool{"mod.prog": true} + m := &Module{ + ForgetDetected: func(key string) error { order = append(order, "delete "+key); delete(kept, key); return nil }, + RecordAct: func(what, why string) error { order = append(order, "log "+why); return nil }, + } + drop := func(why, caller string) (any, error) { + return m.dropDetected(map[string]any{"module": "mod", "artifact": "mod:prog", "drop": true, "why": why}, caller) + } + got, err := drop("a stale answer", "shanks.person") + if a, _ := got.(StepsAnswer); err != nil || a.Dropped != "mod:prog" || kept["mod.prog"] || !reflect.DeepEqual(order, []string{"log a stale answer", "delete mod.prog"}) { + t.Fatalf("drop = %+v, %v, order %v, kept %v", got, err, order, kept) + } + if _, err := drop("", "shanks.person"); err == nil { + t.Error("a drop without why was taken") + } + if _, err := drop("why", "unproven"); err == nil { + t.Error("a drop with no proven caller was taken") + } + kept["mod.prog"], order = true, nil + m.RecordAct = func(string, string) error { return errors.New("log down") } + got, err = drop("again", "shanks.person") + if a, _ := got.(StepsAnswer); err != nil || a.Dropped != "" || !strings.Contains(a.Error, "hand-act log") || !kept["mod.prog"] || len(order) != 0 { + t.Fatalf("a refusing log: %+v, %v, kept %v, order %v", got, err, kept, order) + } +} diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index a3591514d..13a7cc1a1 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,6 +3,8 @@ module meshbuildgo go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 19c0890ae..fa30c8470 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -39,6 +39,7 @@ "mesh-conditions.findings-asked" ], "invokes": [ + "seat:mesh-hand-acts.hand-act", "mesh-delivery-indexer.indexer_read_manifests", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.publish-part", diff --git a/modules/mesh-build-image/cmd/mesh-build-image/asks.go b/modules/mesh-build-image/cmd/mesh-build-image/asks.go index 38a1bda81..141c550b2 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/asks.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/asks.go @@ -77,7 +77,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep // SeatTools are the seat's verbs on this node (ADR 0525 §10). func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool { - return append(buildstep.SeatTools(Seat, "image", p), stepsTool(), m.keptArtifactVerb()) + return append(buildstep.SeatTools(Seat, "image", p), m.stepsTool(), m.keptArtifactVerb()) } // StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go b/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go index e952b09b5..72a70ec14 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go @@ -23,7 +23,7 @@ func TestTheModuleInvokesTheImageSeatAndHoldsNoRegistry(t *testing.T) { if err != nil || json.Unmarshal(raw, &m) != nil { t.Fatal(err) } - if want := []string{"seat:container-image-registry.publish", "seat:container-image-registry.publish-part", + if want := []string{"seat:mesh-hand-acts.hand-act", "seat:container-image-registry.publish", "seat:container-image-registry.publish-part", "seat:container-image-registry.mirror", "seat:mesh-module-assigner.assignments", "seat:mesh-catalog.module"}; !slices.Equal(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } diff --git a/modules/mesh-build-image/cmd/mesh-build-image/handact.go b/modules/mesh-build-image/cmd/mesh-build-image/handact.go new file mode 100644 index 000000000..2903d3b6a --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/handact.go @@ -0,0 +1,13 @@ +package main + +import ( + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// handActRecorder writes an act by hand in the hand-act log of the seat mesh-hand-acts (design 47 G §480); a log that +// does not take it is an error, never a quiet success. +func handActRecorder(what, why string) error { + _, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "build-steps-drop", Verb: "steps drop"}) + return err +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/main.go b/modules/mesh-build-image/cmd/mesh-build-image/main.go index 13224f1b2..f8b4c2e9c 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/main.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/main.go @@ -33,6 +33,7 @@ func main() { ctx := context.Background() m := &Module{ Node: strings.TrimSpace(os.Getenv("MESH_NODE")), + RecordAct: handActRecorder, Steps: stdio.State(seat.StateSteps).Put, ReadSteps: readOf(stdio.State(seat.StateSteps)), Pauses: stdio.State(sdkstep.PausesState).Put, diff --git a/modules/mesh-build-image/cmd/mesh-build-image/module.go b/modules/mesh-build-image/cmd/mesh-build-image/module.go index 73558e4e1..3392d7ff6 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/module.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/module.go @@ -21,6 +21,8 @@ const CheckRound = 10 * time.Minute // Module is the image build module's one running state. type Module struct { Node string + // RecordAct writes an act in the hand-act log of the seat mesh-hand-acts (§463, §480): a drop writes it first. + RecordAct func(what, why string) error // Account and UID are the account the module runs as; Home is its home. Account, UID, Home string // Steps is the Put of the steps state; ReadSteps reads it back for the read tools. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/steps.go b/modules/mesh-build-image/cmd/mesh-build-image/steps.go index e62ff9364..555b5ea6f 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/steps.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/steps.go @@ -7,6 +7,7 @@ package main import ( "fmt" + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage" stdio "git.novox.be/novox/mesh-sdk/go" ) @@ -16,10 +17,13 @@ type StepsAnswer struct { Artifact string `json:"artifact"` Fingerprint string `json:"fingerprint,omitempty"` Steps []seat.DetectedStep `json:"steps"` + // Dropped is the artifact a drop was recorded for; Error why a drop deleted nothing, naming the hand-act log. + Dropped string `json:"dropped,omitempty"` + Error string `json:"error,omitempty"` } // stepsTool is the seat's verb `steps`. -func stepsTool() stdio.Tool { +func (m *Module) stepsTool() stdio.Tool { return stdio.Tool{Name: Seat + "." + seat.VerbSteps, Description: "Which pipeline steps an image artifact can run: none (an image has no lint, unit or integration test step).", Input: map[string]any{"type": "object", "required": []string{"module", "artifact"}, "properties": map[string]any{ @@ -28,14 +32,34 @@ func stepsTool() stdio.Tool { "fingerprint": map[string]any{"type": "string"}, "fetch": map[string]any{"type": "object", "description": "not read: an image's steps need no tree"}, "again": map[string]any{"type": "boolean", "description": "not read: the answer is the same anew"}, + "drop": map[string]any{"type": "boolean", "description": "delete the artifact's kept key instead (an image keeps none); needs why, written in the hand-act log first"}, + "why": map[string]any{"type": "string", "description": "with drop, why: a non-empty text"}, }}, - Run: steps} + Run: m.steps} } -func steps(args map[string]any) (any, error) { +func (m *Module) steps(args map[string]any) (any, error) { str := func(k string) string { s, _ := args[k].(string); return s } if str("artifact") == "" || str("module") == "" { return nil, fmt.Errorf("steps: the module and the artifact are asked") } + if drop, _ := args["drop"].(bool); drop { + return m.dropDetected(args, stdio.CallerOf(args)) + } return StepsAnswer{Artifact: str("artifact"), Fingerprint: str("fingerprint"), Steps: []seat.DetectedStep{}}, nil } + +// dropDetected is `steps` with drop (§463, §480): the act is written in the hand-act log with the caller the bus proved; +// an image keeps no detected steps, so there is no key to delete. +func (m *Module) dropDetected(args map[string]any, caller string) (any, error) { + artifact, _ := args["artifact"].(string) + why, _ := args["why"].(string) + failed, err := buildstep.DropDetected(artifact, why, caller, "", m.RecordAct, nil) + if err != nil { + return nil, err + } + if failed != "" { + return StepsAnswer{Artifact: artifact, Error: failed}, nil + } + return StepsAnswer{Artifact: artifact, Dropped: artifact}, nil +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/steps_test.go b/modules/mesh-build-image/cmd/mesh-build-image/steps_test.go index b24fea3e4..3c1f6dc65 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/steps_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/steps_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "errors" + "strings" + "testing" +) // Design 47 G §22, §328 (BLD8): the verb steps is served and answers none for an image. func TestTheStepsVerbAnswersNoStepForAnImage(t *testing.T) { @@ -22,3 +26,23 @@ func TestTheStepsVerbAnswersNoStepForAnImage(t *testing.T) { t.Fatal("the module serves no verb steps") } } + +// Design 47 G §463, §480 (BI3): steps with drop writes the hand-act log; a log that refuses answers error; a drop +// without why is refused. +func TestTheStepsDropWritesTheHandActLog(t *testing.T) { + var logged []string + m := &Module{RecordAct: func(what, why string) error { logged = append(logged, why); return nil }} + args := map[string]any{"module": "m", "artifact": "m:server", "drop": true, "why": "stale"} + got, err := m.dropDetected(args, "shanks.person") + if a, _ := got.(StepsAnswer); err != nil || a.Dropped != "m:server" || len(logged) != 1 { + t.Fatalf("drop = %+v, %v, logged %v", got, err, logged) + } + if _, err := m.dropDetected(map[string]any{"module": "m", "artifact": "m:server", "drop": true}, "shanks.person"); err == nil { + t.Error("a drop without why was taken") + } + m.RecordAct = func(string, string) error { return errors.New("log down") } + got, _ = m.dropDetected(args, "shanks.person") + if a, _ := got.(StepsAnswer); !strings.Contains(a.Error, "hand-act log") || a.Dropped != "" { + t.Fatalf("a refusing log: %+v", got) + } +} diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index bdd7b9dea..2779db400 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,6 +3,8 @@ module meshbuildimage go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index 4cfac839a..7fefa26e9 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -62,6 +62,7 @@ "node-tools" ], "invokes": [ + "seat:mesh-hand-acts.hand-act", "seat:container-image-registry.publish", "seat:container-image-registry.publish-part", "seat:container-image-registry.mirror", diff --git a/modules/mesh-build-node/cmd/mesh-build-node/handact.go b/modules/mesh-build-node/cmd/mesh-build-node/handact.go new file mode 100644 index 000000000..2903d3b6a --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/handact.go @@ -0,0 +1,13 @@ +package main + +import ( + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// handActRecorder writes an act by hand in the hand-act log of the seat mesh-hand-acts (design 47 G §480); a log that +// does not take it is an error, never a quiet success. +func handActRecorder(what, why string) error { + _, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "build-steps-drop", Verb: "steps drop"}) + return err +} diff --git a/modules/mesh-build-node/cmd/mesh-build-node/main.go b/modules/mesh-build-node/cmd/mesh-build-node/main.go index ad83cc12f..c1abbf75f 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/main.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/main.go @@ -50,6 +50,8 @@ func main() { ReadSteps: readOf(stdio.State(seat.StateSteps)), Detected: stdio.State(seat.StateDetectedSteps).Put, ReadDetected: readOf(stdio.State(seat.StateDetectedSteps)).Get, + ForgetDetected: stdio.State(seat.StateDetectedSteps).Delete, + RecordAct: handActRecorder, Pauses: stdio.State(sdkstep.PausesState).Put, ReadPauses: readOf(stdio.State(sdkstep.PausesState)), ForgetPause: stdio.State(sdkstep.PausesState).Delete, diff --git a/modules/mesh-build-node/cmd/mesh-build-node/module.go b/modules/mesh-build-node/cmd/mesh-build-node/module.go index f87c17b98..8f1aea066 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/module.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/module.go @@ -51,6 +51,10 @@ type Module struct { // Detected and ReadDetected are the Put and the read of the state detected-steps (design 47 G §326, §414). Detected buildstep.Put ReadDetected func(key string) (json.RawMessage, error) + // ForgetDetected deletes one key of detected-steps; RecordAct writes an act in the hand-act log of the seat + // mesh-hand-acts (§463, §480): a drop writes it first. + ForgetDetected func(key string) error + RecordAct func(what, why string) error // Pauses is the Put of the pauses state, which keeps the node's pause key without an age; ReadPauses reads it back. Pauses buildstep.Put ReadPauses StateRead diff --git a/modules/mesh-build-node/cmd/mesh-build-node/steps.go b/modules/mesh-build-node/cmd/mesh-build-node/steps.go index 0f109953f..ca51adb3c 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/steps.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/steps.go @@ -38,6 +38,9 @@ type StepsAnswer struct { Fingerprint string `json:"fingerprint,omitempty"` Steps []DetectedStep `json:"steps"` Kept bool `json:"kept,omitempty"` + // Dropped is the artifact whose key a drop deleted; Error why a drop deleted nothing, naming the hand-act log. + Dropped string `json:"dropped,omitempty"` + Error string `json:"error,omitempty"` } // detectSteps finds the steps of the package directories under dir. @@ -77,26 +80,31 @@ func (m *Module) stepsTool() stdio.Tool { return stdio.Tool{Name: Seat + "." + seat.VerbSteps, Description: "Which pipeline steps a TypeScript artifact can run: the type-check as lint, always; unit tests where a package has a test script; " + "integration tests where one has a test:integration script. A level with no test is not answered.", - Input: map[string]any{"type": "object", "required": []string{"module", "artifact", "fetch"}, "properties": map[string]any{ + Input: map[string]any{"type": "object", "required": []string{"module", "artifact"}, "properties": map[string]any{ "module": map[string]any{"type": "string"}, "artifact": map[string]any{"type": "string", "description": "the artifact reference, :"}, "fingerprint": map[string]any{"type": "string"}, "fetch": map[string]any{"type": "object", "description": "what the fetch tool is asked: the repository, the full commit and the module's directory"}, "again": map[string]any{"type": "boolean", "description": "find them anew, not from the kept answer"}, + "drop": map[string]any{"type": "boolean", "description": "delete the artifact's kept key instead of detecting; needs why (written in the hand-act log first)"}, + "why": map[string]any{"type": "string", "description": "with drop, why: a non-empty text"}, }}, Run: m.Short(m.steps)} } func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) { str := func(k string) string { s, _ := args[k].(string); return s } + if str("artifact") == "" || str("module") == "" { + return nil, fmt.Errorf("steps: the module and the artifact are asked") + } + if drop, _ := args["drop"].(bool); drop { + return m.dropDetected(args, stdio.CallerOf(args)) + } fetch, _ := args["fetch"].(map[string]any) req, err := sdkstep.ParseFetchRequest(fetch) if err != nil { return nil, err } - if str("artifact") == "" || str("module") == "" { - return nil, fmt.Errorf("steps: the module and the artifact are asked") - } artifact, fp := str("artifact"), str("fingerprint") name := artifact if _, n, ok := strings.Cut(artifact, ":"); ok { @@ -126,6 +134,27 @@ func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) { return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: steps}, nil } +// dropDetected is `steps` with drop (§463, §480): the act is written in the hand-act log with the caller the bus proved, +// then the kept key is deleted; a log that refuses leaves the key. +func (m *Module) dropDetected(args map[string]any, caller string) (any, error) { + artifact, _ := args["artifact"].(string) + why, _ := args["why"].(string) + module, _ := args["module"].(string) + name := artifact + if _, n, ok := strings.Cut(artifact, ":"); ok { + name = n + } + key := seat.DetectedStepsByModuleArtifactKey(module, name) + failed, err := buildstep.DropDetected(artifact, why, caller, key, m.RecordAct, m.ForgetDetected) + if err != nil { + return nil, err + } + if failed != "" { + return StepsAnswer{Artifact: artifact, Error: failed}, nil + } + return StepsAnswer{Artifact: artifact, Dropped: artifact}, nil +} + // stepsOfTree fetches the commit alone (nothing installed) into a workspace removed at the end. func (m *Module) stepsOfTree(ctx context.Context, req sdkstep.FetchRequest) ([]DetectedStep, error) { if err := os.MkdirAll(m.Work, 0o700); err != nil { diff --git a/modules/mesh-build-node/cmd/mesh-build-node/steps_test.go b/modules/mesh-build-node/cmd/mesh-build-node/steps_test.go index 0e7accaaa..3593c2d65 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/steps_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/steps_test.go @@ -2,9 +2,11 @@ package main import ( "context" + "errors" "os" "path/filepath" "reflect" + "strings" "testing" ) @@ -51,3 +53,27 @@ func TestTheStepsVerbIsServedAndRefusesAnAskWithoutAnArtifact(t *testing.T) { t.Error("an ask with no artifact was answered") } } + +// Design 47 G §463, §480 (BN9): steps with drop writes the hand-act log first, then deletes the key; a log that +// refuses deletes nothing; a drop without why is refused. +func TestTheStepsDropWritesTheHandActLogFirst(t *testing.T) { + var order []string + m := &Module{ + ForgetDetected: func(key string) error { order = append(order, "delete "+key); return nil }, + RecordAct: func(what, why string) error { order = append(order, "log "+why); return nil }, + } + args := map[string]any{"module": "mod", "artifact": "mod:web", "drop": true, "why": "stale"} + got, err := m.dropDetected(args, "shanks.person") + if a, _ := got.(StepsAnswer); err != nil || a.Dropped != "mod:web" || !reflect.DeepEqual(order, []string{"log stale", "delete mod.web"}) { + t.Fatalf("drop = %+v, %v, order %v", got, err, order) + } + if _, err := m.dropDetected(map[string]any{"module": "mod", "artifact": "mod:web", "drop": true}, "shanks.person"); err == nil { + t.Error("a drop without why was taken") + } + order = nil + m.RecordAct = func(string, string) error { return errors.New("log down") } + got, err = m.dropDetected(args, "shanks.person") + if a, _ := got.(StepsAnswer); err != nil || !strings.Contains(a.Error, "hand-act log") || len(order) != 0 { + t.Fatalf("a refusing log: %+v, %v, order %v", got, err, order) + } +} diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index af385b3dd..cb074bd3b 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,6 +3,8 @@ module meshbuildnode go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index 1afa7b1a6..a056ac734 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -75,6 +75,7 @@ "npm-package-registry": "${dir:state}/package-registry.json" }, "invokes": [ + "seat:mesh-hand-acts.hand-act", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.publish-part", "seat:npm-package-registry.publish",