From 7efb17386d6f8fcc5bd35e40ed1a1f2f2ac545d4 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:14:43 +0100 Subject: [PATCH] Build log: a build narrates between its start and its end (build.said) Its module, fingerprint, line and why, as design 51 has a build narrate its publish; a start or end carries no line (A3g's finding). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/delivery/history_records.go | 19 ++++++++++++++++++- go/delivery/published_packages_test.go | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/go/delivery/history_records.go b/go/delivery/history_records.go index 1ce6146..8f8f23a 100644 --- a/go/delivery/history_records.go +++ b/go/delivery/history_records.go @@ -132,6 +132,10 @@ const ( BuildLogRead = BuilderModule + "." + BuilderLog BuildStartedEvent = "build.started" BuildEndedEvent = "build.ended" + // BuildSaidEvent is a build's narration between its start and its end (design 51: a build narrates its publish, + // asked, waiting on a seat, stored, refused, at least every five minutes while it waits): its module, fingerprint, + // line and why. + BuildSaidEvent = "build.said" // BuilderLogDays is how long the log keeps its entries. BuilderLogDays = 30 // BuildsSinceTool is the builder's tool answering every ended build since a time, in order (clause 9): its @@ -175,6 +179,9 @@ type BuildLogEntry struct { // sorted and each once, as the build module answered them (BuildAnswer.Imports; novox/hq ADR 0593 ยง2): the indexer // keeps them on the module (KeptInputs2.Imports) when the build's fingerprint is the module's. Imports []string `json:"imports,omitempty"` + // Line is a narration's words, Why what it narrates (asked, waiting, stored, refused): a build.said entry's alone. + Line string `json:"line,omitempty"` + Why string `json:"why,omitempty"` } // BuildsSince is the answer of BuildsSinceTool: every ended build since a time, oldest first, and whether the log held @@ -214,8 +221,18 @@ func EndedSince(entries []BuildLogEntry, since time.Time) []BuildLogEntry { func EncodeBuildLogEntry(e BuildLogEntry) ([]byte, error) { e.V = Version switch { + case e.Event == BuildSaidEvent: + switch { + case e.Module == "" || e.FP == "" || e.At.IsZero() || e.Line == "": + return nil, fmt.Errorf("delivery: a build's narration names its module, fingerprint, line and time") + case e.Outcome != "" || len(e.Made) > 0 || e.Manifest != "" || len(e.Against) > 0 || len(e.Imports) > 0: + return nil, fmt.Errorf("delivery: a build's narration names no outcome, manifest, base, artifact or import") + } + return json.Marshal(e) case e.Event != BuildStartedEvent && e.Event != BuildEndedEvent: - return nil, fmt.Errorf("delivery: a build log entry is %s or %s, not %q", BuildStartedEvent, BuildEndedEvent, e.Event) + return nil, fmt.Errorf("delivery: a build log entry is %s, %s or %s, not %q", BuildStartedEvent, BuildSaidEvent, BuildEndedEvent, e.Event) + case e.Line != "" || e.Why != "": + return nil, fmt.Errorf("delivery: a build's start or end carries no narration's line") case e.Module == "" || e.FP == "" || e.Node == "" || e.At.IsZero() || e.Repository == "" || e.Commit == "": return nil, fmt.Errorf("delivery: a build log entry names its module, fingerprint, repository, commit, node and time") case e.Event == BuildEndedEvent && e.Outcome == "": diff --git a/go/delivery/published_packages_test.go b/go/delivery/published_packages_test.go index a8b2240..bd44b58 100644 --- a/go/delivery/published_packages_test.go +++ b/go/delivery/published_packages_test.go @@ -75,6 +75,28 @@ func TestPublishedPackagesAndImportsRoundTripAndRefuse(t *testing.T) { if _, err := EncodeBuildLogEntry(end); err == nil { t.Error("unsorted imports encoded") } + // A build narrates between its start and its end (build.said): its module, fingerprint, line and why, nothing of an + // end; a start or end carries no line. + said := BuildLogEntry{Event: BuildSaidEvent, Module: "netcheck", FP: "src2:x", At: good.At, + Line: "waiting on container-image-registry", Why: "waiting"} + if raw, err := EncodeBuildLogEntry(said); err != nil { + t.Errorf("a narration refused: %v", err) + } else if e, err := DecodeBuildLogEntry(raw); err != nil || e.Line != said.Line || e.Why != "waiting" { + t.Errorf("a narration's round trip: %+v, %v", e, err) + } + for name, mutate := range map[string]func(e *BuildLogEntry){ + "a narration with no line": func(e *BuildLogEntry) { e.Line = "" }, + "a narration with an outcome": func(e *BuildLogEntry) { e.Outcome = "done" }, + "an end with a line": func(e *BuildLogEntry) { + e.Event, e.Outcome, e.Node, e.Repository, e.Commit = BuildEndedEvent, "done", "ace", "novox/mesh-catalog", commit + }, + } { + e := said + mutate(&e) + if _, err := EncodeBuildLogEntry(e); err == nil { + t.Errorf("%s encoded", name) + } + } var answer BuildAnswer if err := json.Unmarshal([]byte(`{"artifacts":[],"imports":["`+sdk+`"]}`), &answer); err != nil || len(answer.Imports) != 1 { t.Fatalf("build answer imports: %v, %v", err, answer.Imports)