BLD2: the Go build seat keeps the answer of steps in detected-steps by fingerprint

Part of BLD2 (Go module): the state detected-steps asked by the template
state, one key per artifact, answered for the same fingerprint, replaced by a
newer one and by again. The drop of a key and the Node and image modules wait
(drop: question to the lead; the others follow in BLD7 and BLD8).

Builds: design 47 G §326, §414

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 10:56:55 +01:00
parent cfad86394f
commit 25bbd55539
7 changed files with 79 additions and 3 deletions
@@ -15,6 +15,8 @@ var Kept = []announce.State{
{Name: "keys", Readable: true},
{Name: "steps", Readable: true},
{Name: "pauses", Readable: true},
// The steps detected for each artifact, kept by its fingerprint (design 47 G §326, §414).
{Name: "detected-steps", Readable: true},
// The condition keeper raises a first view of the indexer's manifests it could not get (ADR 0615 §6) from this.
{Name: "findings", Readable: true},
}
@@ -10,7 +10,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"reflect"
"strings"
"testing"
@@ -147,7 +147,9 @@ func TestManifestRequiresForgeAccountKeepsKeysAndTakesFilesFromTemplates(t *test
m := readForgeManifest(t)
// node-tools: the module pulls its asks through the tool runner's seat pull (1911).
// go-package-registry: the Go seat's read side, the fetch's first source (design 51, CAT-BG3, E2).
if want := []any{"forge-account", "go-toolchain", "go-package-registry", "node-tools"}; !slices.Equal(m.Requires, want) {
// detected-steps: the state of the verb steps (design 47 G §326, §414), asked by the template state.
detected := map[string]any{"name": "detected-steps", "storage": "key-value", "template": "state", "options": map[string]any{"history": float64(1)}}
if want := []any{detected, "forge-account", "go-toolchain", "go-package-registry", "node-tools"}; !reflect.DeepEqual(m.Requires, want) {
t.Errorf("requires = %v, want %v", m.Requires, want)
}
keeps := false
@@ -41,6 +41,8 @@ func main() {
Toolchains: stdio.State(seat.StateToolchains).Put,
Emit: func(t string, b any) error { return stdio.Emit(t, b) },
Steps: stdio.State(seat.StateSteps).Put,
Detected: stdio.State(seat.StateDetectedSteps).Put,
ReadDetected: readOf(stdio.State(seat.StateDetectedSteps)).Get,
ReadSteps: readOf(stdio.State(seat.StateSteps)),
Pauses: stdio.State(sdkstep.PausesState).Put,
ReadPauses: readOf(stdio.State(sdkstep.PausesState)),
@@ -33,6 +33,10 @@ type Module struct {
// Held is the puller whose running asks a step-end tool will not end (issue 1890); nil in a test.
Held *buildstep.Puller
ReadSteps, ReadToolchainsState StateRead
// Detected and ReadDetected are the Put and the read of the state detected-steps (design 47 G §326, §414): the
// answer of the verb steps, one key per artifact.
Detected buildstep.Put
ReadDetected func(key string) (json.RawMessage, 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
@@ -158,11 +158,35 @@ func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) {
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
// and by again (§326).
if again, _ := args["again"].(bool); !again && fp != "" && m.ReadDetected != nil {
if raw, err := m.ReadDetected(key); err == nil && len(raw) > 0 {
if kept, err := seat.DecodeDetectedStepsByModuleArtifactValue(raw); err == nil && kept.Fingerprint == fp {
return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: kept.Steps, Kept: true}, nil
}
}
}
steps, err := m.stepsOfTree(ctx, req)
if err != nil {
return nil, err
}
return StepsAnswer{Artifact: str("artifact"), Fingerprint: str("fingerprint"), Steps: steps}, nil
if fp != "" && m.Detected != nil {
if _, err := m.Detected(key, seat.DetectedSteps{V: 1, Fingerprint: fp, Steps: steps, At: ptr(m.now())}); err != nil {
return nil, fmt.Errorf("steps: keeping the answer: %w", err)
}
}
return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: steps}, nil
}
// artifactName is the artifact of an artifact reference <module>:<artifact>.
func artifactName(ref string) string {
if _, name, ok := strings.Cut(ref, ":"); ok {
return name
}
return ref
}
// stepsOfTree fetches the commit into a workspace removed at the end and detects the steps of the module's directory.
@@ -196,3 +220,5 @@ func (m *Module) stepsOfTree(ctx context.Context, req sdkstep.FetchRequest) ([]D
}
return detectSteps(dir)
}
func ptr[T any](v T) *T { return &v }
@@ -1,6 +1,8 @@
package main
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"reflect"
"testing"
@@ -48,3 +50,40 @@ func TestTheStepsVerbAnswersTheStepsOfAFixtureArtifact(t *testing.T) {
t.Fatalf("a module with no test: %+v, %v", got, err)
}
}
// Design 47 G §326 (BLD2): steps answers from the kept key for the same fingerprint, a newer fingerprint and again
// replace it.
func TestTheStepsVerbKeepsItsAnswerByFingerprint(t *testing.T) {
f := &fakeStep{files: map[string]string{"go.mod": "module example.test/mod\n", "a/a_test.go": "package a\n"}}
m, _ := fetchModule(t, f)
kept := map[string]json.RawMessage{}
m.Detected = func(key string, v any) (uint64, error) {
raw, err := json.Marshal(v)
kept[key] = raw
return 1, err
}
m.ReadDetected = func(key string) (json.RawMessage, error) { return kept[key], nil }
call := func(fp string, again bool) StepsAnswer {
t.Helper()
got, err := m.steps(context.Background(), map[string]any{"module": "mod", "artifact": "mod:prog", "fingerprint": fp, "again": again,
"fetch": map[string]any{"repository": "novox/mod", "commit": commitSHA}})
if err != nil {
t.Fatal(err)
}
return got.(StepsAnswer)
}
if a := call("fp1", false); a.Kept || len(kept) != 1 || kept["mod.prog"] == nil {
t.Fatalf("first ask: %+v, keys %v", a, kept)
}
f.files = map[string]string{"go.mod": "module example.test/mod\n"} // the tree changed, the kept answer still answers fp1
if a := call("fp1", false); !a.Kept || len(a.Steps) != 2 {
t.Fatalf("same fingerprint: %+v", a)
}
if a := call("fp1", true); a.Kept || len(a.Steps) != 1 {
t.Fatalf("again: %+v", a)
}
f.files = map[string]string{"go.mod": "module example.test/mod\n", "a/a_test.go": "package a\n"}
if a := call("fp2", false); a.Kept || len(a.Steps) != 2 {
t.Fatalf("newer fingerprint: %+v", a)
}
}
+1
View File
@@ -82,6 +82,7 @@
}
],
"requires": [
{"name": "detected-steps", "storage": "key-value", "template": "state", "options": {"history": 1}},
"forge-account",
"go-toolchain",
"go-package-registry",