DLV-B10: the builder's three narrations are build.said entries of its build log

Design 51 says the build log tells a version kept and why, a tag ask unanswered after five minutes,
and the old form's window closing; the process log alone was read by nobody. Each numbering decision
is now a build.said of its module and fingerprint, its why one of numbered, kept, waiting, unnumbered;
the tag ask and the window, no one build's, are said under the builder's own key, the ask's or the
window's key standing for the fingerprint. Needs mesh-sdk 7efb173 (build.said). Each failed first.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:26:15 +01:00
parent 321c4e4122
commit 3a64fde7d9
7 changed files with 99 additions and 12 deletions
@@ -541,7 +541,15 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
}
// 1955/L7 (ADR 0571 §1): the build's start and end are in the module log build-log under the module's key, read as a
// reader with the log's read reads them, the end naming the manifest and what was made.
ended := b.logged(t, "netcheck")
// DLV-B10: between them the build log holds the build's narrations (build.said), each version numbered, and why.
var ended []delivery.BuildLogEntry
for _, e := range b.logged(t, "netcheck") {
if e.Event != delivery.BuildSaidEvent {
ended = append(ended, e)
} else if e.Why != versions.SaidNumbered || !strings.Contains(e.Line, "numbered 0.1.5") || e.FP != fp {
t.Errorf("said = %+v, want netcheck's versions numbered", e)
}
}
if len(ended) != 2 || ended[0].Event != delivery.BuildStartedEvent || ended[1].Event != delivery.BuildEndedEvent ||
ended[1].Manifest != digest || len(ended[1].Made) != 2 || ended[1].Node != "n1" {
t.Errorf("logged = %+v, want the start and the end of netcheck's build", ended)
@@ -555,6 +563,13 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
if !strings.Contains(said, "the check's build of abc is not claimed yet") {
t.Errorf("the narration = %q, want what the ask waits on", said)
}
// And in the build log, under the builder's own key (the ruling of 13:17: build.said).
if !slices.ContainsFunc(b.logged(t, delivery.BuilderModule), func(e delivery.BuildLogEntry) bool {
return e.Event == delivery.BuildSaidEvent && e.Why == versions.SaidUnanswered && e.FP == key && e.Commit == "abc" &&
strings.Contains(e.Line, "the check's build of abc is not claimed yet")
}) {
t.Errorf("the build log of %s = %+v, want the unanswered tag ask said", delivery.BuilderModule, b.logged(t, delivery.BuilderModule))
}
if e, err := b.tagAsks.Get(b.ctx, key); err != nil {
t.Fatal(err)
} else if a, _ := versions.DecodeTagAsk(e.Value()); a.Answered.IsZero() || !a.Unanswered() {
@@ -601,6 +616,11 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) // the indexer writes it again
b.awaitFP(t, "netcheck", third, isState(delivery.BuildDone))
b.awaitSaid(t, "kept: fingerprint "+third+" is numbered under it")
if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e delivery.BuildLogEntry) bool {
return e.Event == delivery.BuildSaidEvent && e.Why == versions.SaidKept && e.FP == third && strings.Contains(e.Line, "kept: fingerprint "+third)
}) {
t.Errorf("the build log of netcheck says no version kept at %s", third)
}
reg.awaitFinding(t, failed, false)
// DLV-B4: the seats' refused and removed, heard on the builder's durable consumer, move a numbered version: a retention
// removal turns the entry removed with its reason, a refusal refused with the holder's reason and the bytes it refused.
@@ -155,7 +155,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
b.Numbers = build.VersionNumbers{N: numberer, TagNumbered: tagNumbered(tagAsksKV.KV, logger.Printf, time.Now)}
// An ask not answered within five minutes is narrated with what it waits on: its bound's end on the bus says when.
boundsKV := process.AwaitBucket(ctx, js, conn, versions.TagAskBoundsBucket, "an unanswered tag ask is not narrated", logger.Printf)
bounds := &tagBounds{asks: tagAsksKV.KV, bounds: boundsKV.KV, bound: tagAskBound, waits: floorWaits.Waiting, logf: logger.Printf, now: time.Now}
bounds := &tagBounds{asks: tagAsksKV.KV, bounds: boundsKV.KV, bound: tagAskBound, waits: floorWaits.Waiting, logf: logger.Printf, now: time.Now,
log: func(e delivery.BuildLogEntry) { b.Log(e) }}
// A state bound later numbers the builds looked at without it.
versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) })
// The versions' hand acts (builder_publish_again, builder_floor_again) come through reasks, as builder_build_again.
@@ -185,6 +186,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the
// indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since.
b.Log = func(e delivery.BuildLogEntry) {
if e.Node == "" { // a narration of no one build (a tag ask, the old form's window) is this instance's
e.Node = cred.Node
}
if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil {
logger.Printf("%v", err)
}
@@ -225,7 +229,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form
// reads it and drops a retired node by hand.
reads.ctx = context.WithoutCancel(ctx)
reads.window = &oldForm{forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now,
reads.window = &oldForm{forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e delivery.BuildLogEntry) { b.Log(e) },
said: func(body versions.OldFormClosedBody) {
if err := emitEvent(context.WithoutCancel(ctx), js, cred, versions.OldFormClosedEvent, "old-form-closed-"+body.At.Format(time.RFC3339Nano), body); err != nil {
logger.Printf("saying %s: %v", versions.OldFormClosedEvent, err)
@@ -12,6 +12,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-delivery/sdk/forms"
@@ -28,6 +29,8 @@ type oldForm struct {
findings *findings
// said emits version.old-form-closed.
said func(versions.OldFormClosedBody)
// log writes the close into the builder's build log (build.said); nil writes none.
log func(delivery.BuildLogEntry)
logf func(string, ...any)
now func() time.Time
@@ -103,8 +106,15 @@ func (o *oldForm) step(ctx context.Context, counted map[string][]string) {
o.overdue = overdue
if closed {
body := next.Closing()
o.logf("the old reference form's window closed at %s: no counted node-engine reads only an older form (last held by %s; dropped by hand: %d); new build keys leave it out",
line := fmt.Sprintf("the old reference form's window closed at %s: no counted node-engine reads only an older form (last held by %s; dropped by hand: %d); new build keys leave it out",
body.At.Format(time.RFC3339), strings.Join(body.LastHolding, ", "), len(body.Dropped))
o.logf("%s", line)
// In the build log too (design 51 *The node-engine's own build*), under the builder's own key, the window's key
// standing for the fingerprint: the close is no one build's.
if o.log != nil {
o.log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: delivery.BuilderModule, FP: versions.OldFormKey,
At: body.At.UTC(), Line: line, Why: versions.SaidOldFormClosed})
}
if o.said != nil {
o.said(body)
}
@@ -344,8 +344,10 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
now := time.Unix(0, 0)
fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}}
var closings []versions.OldFormClosedBody
var logged []delivery.BuildLogEntry
window := &oldForm{forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now },
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) }}
said: func(b versions.OldFormClosedBody) { closings = append(closings, b) },
log: func(e delivery.BuildLogEntry) { logged = append(logged, e) }}
reads := &engineReads{keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}}
newest := []string{forms.FormNeed("oci")}
counted := map[string][]string{"a": newest, "b": nil}
@@ -365,6 +367,12 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
if window.w.Open || len(closings) != 1 || !slices.Equal(closings[0].LastHolding, []string{"b"}) || st.vals[heldKey] != nil {
t.Errorf("the node back with the newest form: %+v, closings %+v, finding left %v", window.w, closings, st.vals[heldKey] != nil)
}
if len(logged) != 1 || logged[0].Event != delivery.BuildSaidEvent || logged[0].Why != versions.SaidOldFormClosed ||
!strings.Contains(logged[0].Line, "last held by b") {
t.Errorf("the build log = %+v, want the close said once (build.said)", logged)
} else if _, err := delivery.EncodeBuildLogEntry(logged[0]); err != nil {
t.Errorf("the close is no entry the build log takes: %v", err)
}
if _, err := window.drop(context.Background(), "b", "op@shanks", "retired", now); err != nil {
t.Fatal(err)
}
@@ -8,6 +8,8 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-builder/internal/versions"
)
@@ -19,8 +21,10 @@ type tagBounds struct {
bounds func() jetstream.KeyValue
waits func() []versions.FloorWait
logf func(string, ...any)
now func() time.Time
bound time.Duration
// log writes a narration into the builder's build log (build.said); nil writes none.
log func(delivery.BuildLogEntry)
now func() time.Time
bound time.Duration
}
// arm writes key's bound when none stands: a Create, so a standing bound keeps its time and a restart arms each
@@ -73,7 +77,14 @@ func (tb *tagBounds) ended(ctx context.Context, e jetstream.KeyValueEntry) {
if tb.waits != nil {
waits = tb.waits()
}
tb.logf("%s", versions.NarrateUnanswered(t, waits, tb.now()))
line := versions.NarrateUnanswered(t, waits, tb.now())
tb.logf("%s", line)
// In the build log too (design 51 *Tagged versions*): under the builder's own key, no build being claimed yet, the
// ask's key standing for the fingerprint.
if tb.log != nil {
tb.log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: delivery.BuilderModule, FP: e.Key(), Commit: t.Head,
At: tb.now().UTC(), Line: line, Why: versions.SaidUnanswered})
}
tb.arm(ctx, e.Key())
}
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -74,7 +75,12 @@ func (b *Builder) number(ctx context.Context, module string, m delivery.Module,
}
got, err := b.Numbers.Number(ctx, module, m, plan, tag)
for _, s := range got.Said {
b.logf("%s at %s: %s", module, m.FP2, s)
b.logf("%s at %s: %s", module, m.FP2, s.Line)
// And in the build log (design 51 *The versions*: it says a version was kept, and why), between the build's start and end.
if b.Log != nil {
b.Log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: module, FP: m.FP2, Commit: m.Commit, Node: b.Node,
At: time.Now().UTC(), Line: s.Line, Why: s.Why})
}
}
if err != nil {
// Said, and built as before: an unnumbered build is published by its digest (ADR 0617 §1), numbered at its next look.
@@ -74,8 +74,36 @@ type Numbering struct {
Unnumbered []string
// Wait is why the build waits: a package whose floor is not known (design 51 *The floor*). Nothing is claimed.
Wait string
// Said are the builder's narrations of each decision, for its log.
Said []string
// Said are the builder's narrations of each decision, for its process log and its build log (build.said).
Said []Narration
}
// Narration is one line the builder says in its build log (design 51: the build log says a version was kept, and
// why), and what it narrates.
type Narration struct{ Line, Why string }
// What a narration of the build log narrates (the build.said entry's why).
const (
SaidNumbered = "numbered"
SaidKept = "kept"
SaidWaiting = "waiting"
SaidUnnumbered = "unnumbered"
SaidUnanswered = "tag ask unanswered"
SaidOldFormClosed = "old-form-closed"
)
// narrationOf is a decision's narration.
func narrationOf(name string, d Decision) Narration {
why := SaidNumbered
switch {
case d.Keep:
why = SaidKept
case d.Wait:
why = SaidWaiting
case d.Unnumbered:
why = SaidUnnumbered
}
return Narration{Line: name + ": " + d.Why, Why: why}
}
// Numberer numbers trunk builds at their claim (design 51 *The versions*, DLV-B2, B3): it asks a name's floor only
@@ -118,7 +146,7 @@ func (n *Numberer) Number(ctx context.Context, c BuildClaim) (Numbering, error)
if err != nil {
return out, err
}
out.Said = append(out.Said, name+": "+d.Why)
out.Said = append(out.Said, narrationOf(name, d))
switch {
case d.Wait:
n.wait(name, seat, floor, why)