From d15d66fc47c8c00aa9b4b7a3c4b035463ca16908 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 14:35:59 +0100 Subject: [PATCH] DLV-I20 (builder): a build's end names the generated files its build module found out of step The builder reads out-of-step from each lane's step result, a failed step's too, and writes it on the build's end entry (BuildLogEntry.OutOfStep, J3's mesh-sdk field), where the indexer raises generated-types-out-of-step for a trunk (the lead's ruling on A2i's finding 2). BuildAnswer names no such field yet, so it is read by name and shape. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../internal/build/imports_test.go | 42 +++++++++++++++ .../internal/build/run.go | 53 +++++++++++++++++-- .../internal/build/run_test.go | 1 + 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-builder/internal/build/imports_test.go b/modules/mesh-delivery-builder/internal/build/imports_test.go index 4b7d27e4..77215db7 100644 --- a/modules/mesh-delivery-builder/internal/build/imports_test.go +++ b/modules/mesh-delivery-builder/internal/build/imports_test.go @@ -34,3 +34,45 @@ func importsABuildAnswersReachTheDoneKeyAndTheLog(t *testing.T) { t.Fatalf("log %+v, want the imports on the end alone", logged) } } + +// Design 26 (the lead's ruling on A2i's finding 2): the out-of-step files a contracts module's build module answers, in a +// failed step's result too, reach the log's end entry, sorted and each once, where the indexer raises its finding; a +// build that answers none names none. +func outOfStepFilesABuildAnswersReachTheLog(t *testing.T) { + for _, row := range []struct { + name string + fail bool + files []string + want string + }{ + {"a passed build naming files", false, []string{"go/b.go", "go/a.go", "go/b.go"}, "go/a.go,go/b.go"}, + {"a failed build naming files", true, []string{"ts/x.ts"}, "ts/x.ts"}, + {"a build naming none", false, nil, ""}, + } { + t.Run(row.name, func(t *testing.T) { + r := newRig(t, goBundle) + r.asks.Answer = func(a delivery.StepAsk) delivery.Step { + step := r.answer(a) + answer := map[string]any{} + _ = json.Unmarshal(step.Result, &answer) + if row.files != nil { + answer["out-of-step"] = row.files + } + step.Result, _ = json.Marshal(answer) + if row.fail { + step.State, step.Reason = delivery.StepFailed, "the generated files are out of step" + } + return step + } + var logged []delivery.BuildLogEntry + var mu sync.Mutex + r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } + r.consider(t, "modules/m") + mu.Lock() + defer mu.Unlock() + if len(logged) != 2 || strings.Join(logged[1].OutOfStep, ",") != row.want || len(logged[0].OutOfStep) != 0 { + t.Fatalf("log %+v, want the end naming %q", logged, row.want) + } + }) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 106916cf..12eca8e3 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -90,8 +90,10 @@ type Builder struct { plans map[string]planned // by manifest digest cands map[string]delivery.Candidate // by candidates key (candidates.go) via map[string]via // by build key: a candidate's fetch (candidates.go) - trunkFP map[string]bool // by build key: a trunk module value carries this fingerprint - tried map[string]bool // by build key: looked at since this process started + // outOfStep are, by build key, the out-of-step files a build's lanes answered, until its end entry takes them. + outOfStep map[string][]string + trunkFP map[string]bool // by build key: a trunk module value carries this fingerprint + tried map[string]bool // by build key: looked at since this process started // published are the package versions `@` a done key of this bucket names as a package's reference: a key // waiting for one is built once it is here (ADR 0500 clause 6). published map[string]bool @@ -490,7 +492,9 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { b.logf("%s: the build could not be written: %v", cl.Key, err) return } - b.logBuild(LogEnded(module, m, b.refOf(module, m.FP2), b.Node, b.now(), result)) + ended := LogEnded(module, m, b.refOf(module, m.FP2), b.Node, b.now(), result) + ended.OutOfStep = b.takeOutOfStep(key) // the generated files the build module's compare found out of step (design 26) + b.logBuild(ended) if result.State == delivery.BuildFailed { b.logf("%s: failed: %s", cl.Key, result.Error) } @@ -536,6 +540,7 @@ type lane struct { digests map[string]string // artifact -> digest references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) imports []string // the mesh packages the lane's programs import, as its build module answered them + outOfStep []string // the generated files its build module's compare found out of step, passed or failed err error } @@ -591,6 +596,9 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p go func() { defer wg.Done(); b.runLane(ctx, l, repository, m, module, asked) }() } wg.Wait() + if key, err := delivery.BuilderBuildKey(module, m.FP2); err == nil { + b.keepOutOfStep(key, lanes) + } var errs []string for _, l := range lanes { if l.err != nil { @@ -861,6 +869,15 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del return } l.node = step.Node + // The build module's compare of a contracts module's generated files (design 26, CAT-BG5/BN2) answers the files out + // of step as `out-of-step`, in a failed step's result too. Read beside the build's answer: the SDK's BuildAnswer + // names no such field yet (a request for mesh-sdk), so it is read by its name and shape here. + var compared struct { + OutOfStep []string `json:"out-of-step"` + } + if len(step.Result) > 0 && json.Unmarshal(step.Result, &compared) == nil { + l.outOfStep = compared.OutOfStep + } if step.State != delivery.StepPassed { l.err = fmt.Errorf("%s on %s: the ask %s %s: %s", l.module, step.Node, ask.ID, step.State, step.Reason) return @@ -959,6 +976,36 @@ func LogEnded(module string, m delivery.Module, ref, by string, at time.Time, re return e } +// keepOutOfStep holds, by build key, the out-of-step files the lanes' build modules answered, sorted and each once, until +// the build's end entry takes them (takeOutOfStep); a build with none holds nothing. +func (b *Builder) keepOutOfStep(key string, lanes []*lane) { + seen := map[string]bool{} + for _, l := range lanes { + for _, f := range l.outOfStep { + seen[f] = true + } + } + b.mu.Lock() + defer b.mu.Unlock() + if len(seen) == 0 { + delete(b.outOfStep, key) + return + } + if b.outOfStep == nil { + b.outOfStep = map[string][]string{} + } + b.outOfStep[key] = slices.Sorted(maps.Keys(seen)) +} + +// takeOutOfStep answers the out-of-step files held for a build key and forgets them. +func (b *Builder) takeOutOfStep(key string) []string { + b.mu.Lock() + defer b.mu.Unlock() + files := b.outOfStep[key] + delete(b.outOfStep, key) + return files +} + // importsOf are the mesh packages the lanes' programs import, as their build modules answered them (novox/hq ADR 0593 // ยง2): sorted and each once; nil when none answered any. A lane kept from a prior build answered none, so a build that // kept one reports only what it built. diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 3a042d25..003bda37 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -222,6 +222,7 @@ var ( func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7, ADR 0478 clauses 1, 4 t.Run("the imports a build answers reach the done key and the log", importsABuildAnswersReachTheDoneKeyAndTheLog) + t.Run("the out-of-step files a build answers reach the log", outOfStepFilesABuildAnswersReachTheLog) r := newRig(t, goBundle, archiveA, tsBundle) // ADR 0584: a third node agrees with novox and ace's typescript is the odd one; the build is done and its step says ace sits out. r.tools.Put(buildrun.NodeBuildModule, "g14.typescript", tcValue(t, "typescript", "tsc5"), false)