Builder: the manifest names no other module, the floor reads the seat git and the catalog half

The floor's tags are the seat git's verb tags and its requirements what the catalog half's entries
name for the package (indexer_requirements and the forge bridge's bridge_tags go from code and
manifest); a catalog revision the builder hears wakes a silent floor (the forge stream's consume
and its wake go). The guard test says A-107 relaxes ADR 0437 section 8 for a seat's verbs and
accepted kinds. The integration bed creates the state bounds the step bounds are kept in.
The assignments view's seat form (A-119 (3)) is not built here.

Builds: design 47 G §7, A-82, A-107, A-119
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:18:40 +01:00
parent fd8f019751
commit 1eb481ccc8
14 changed files with 111 additions and 127 deletions
@@ -75,6 +75,8 @@ type catalogHalf struct {
halves map[string]*views.Follower
once sync.Once
ctx context.Context
// floor is told each entry change: a floor whose tags or requirements were silent is asked again (design 47 G A-119).
floor func(context.Context)
// holders is told each entry change: a claim of a build seat may have moved (design 47 G A-82).
holders func()
}
@@ -112,6 +114,9 @@ func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, e
c.logf("%v", err)
}
c.b.Entry(ctx, origin, e)
if c.floor != nil {
defer c.floor(ctx)
}
c.mu.Lock()
was := c.named[module]
now := namesOf(e)
@@ -291,6 +291,10 @@ func startBedWith(t *testing.T, closed bool) *bed {
viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...)
b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1)
// The state bounds, whose keys have each its own time to live: an ask's step bounds (design 47 G A-77, A-118) are kept in it.
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: versions.BoundsBucket, History: 1, LimitMarkerTTL: time.Second}); err != nil {
t.Fatal(err)
}
b.out = &saidLog{}
// The builder's module log, as the runtime raises it (ADR 0297): its stream on the log's subjects.
logStream := buscompose.LogStreamName(builds.BuilderModule, builds.BuilderLog)
@@ -1043,7 +1047,7 @@ func TestARestartedBuilderPutsNoSecondAsk(t *testing.T) {
// registry is the part of the bed design 51's builder cases need: the builder's states versions, findings, floor-waits and
// reasks, EVENTS with the seat oci-artifact-registry's outcomes and the builder's durable consumer of it, the builder's
// own events kept, and the floor's three sources served on the bus as their holders answer through the tool runner
// ({result}): the seat's versions, the forge bridge's bridge_tags and the indexer's indexer_requirements.
// ({result}): the seat's versions, the seat git's tags and the indexer's indexer_requirements.
type registry struct {
b *bed
versions, findings, reasks jetstream.KeyValue
@@ -1066,7 +1070,6 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis
// EVENTS with two seats' outcomes, and the forge bridge's stream (a branch's end): each with the builder's durable consumer.
for stream, subjects := range map[string][]string{
eventsStream: {"mesh.mod.*.event.>", "mesh.seat.*.event.>"},
forgeStream: {process.ModuleStreamSubject(forgeBridgeModule, ">")},
} {
if _, err := b.js.CreateOrUpdateStream(b.ctx, jetstream.StreamConfig{Name: stream, Subjects: subjects}); err != nil {
t.Fatal(err)
@@ -1093,7 +1096,7 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis
}
for subject, answer := range map[string]any{
"mesh.seat." + ociartifactregistry.Seat + ".call.versions.>": map[string]any{"versions": listed},
"mesh.mod.mesh-delivery-forge-bridge.call.bridge_tags.>": map[string]any{"tags": []string{}},
"mesh.seat.git.call.tags.>": map[string]any{"tags": []string{}},
"mesh.mod." + indexerModule + ".call.indexer_requirements.>": map[string]any{"requirements": []string{}},
} {
reply, _ := json.Marshal(map[string]any{"result": answer, "node": "ctl"})
@@ -5,7 +5,6 @@
package main
import (
"cmp"
"context"
"errors"
"fmt"
@@ -168,7 +167,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
floorWaits.Keep(kvKeeper{ctx: ctx, kv: kv}, heldFloorWaits(ctx, kv, logger.Printf))
})
// A package is numbered under its name in its ecosystem, read from the catalog half's entries (design 32, design 47 G §348).
numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call, versions.HolderOf(branches)), Waits: floorWaits,
numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call, versions.RequirementsFrom(versions.EntriesOf(branches))), Waits: floorWaits,
Names: versions.NamesFrom(versions.EntriesOf(branches))}
// A publish that ends badly is a finding (design 51 *Health and conditions*): a holder's refused, a version no holder
// answered within its bound (a key of the state bounds with its own time to live), a seat's outcome that does not read.
@@ -367,22 +366,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// Each consumer that is not there yet is the finding family-not-heard naming its stream until it binds at a later
// start (heard.Writer; ADR 0615 §5); one there that cannot be heard stops the builder, as before.
notHeard := found.notHeard(ctx)
var stopped error
hear := func(stream, consequence string, each func(jetstream.Msg)) {
_ = notHeard.Bind(stream, consequence, func() error {
cons, err := cred.EventConsumer(ctx, js, stream)
if err != nil {
return fmt.Errorf("the consumer %s_%s on %s does not exist yet: %w", cred.Node, cred.Module, stream, err)
}
cc, err := process.HearEvents(cons, each, logger.Printf)
if err != nil {
stopped = cmp.Or(stopped, err)
return err
}
go func() { <-ctx.Done(); cc.Stop() }()
return nil
})
}
// The seat mesh-artifact-sweep says artifacts-gone when it found a key's blob gone (issue 1937, ADR 0571 §2): the
// builder's durable consumer on EVENTS, made by the grants part from the manifest's consumes. Where it is not there yet the builder says
// so; it still asks held at its start and at every module value.
@@ -425,9 +408,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
}
hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4)
if stopped != nil {
return stopped
}
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf))
// Every part has registered: the hub pulls the module's durable. Where the consumer is not there yet the builder says so
// and binds it at a later change (heard.Writer; ADR 0615 §5).
@@ -503,13 +483,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
go func() { errs <- follow(ctx, qw, eachRead) }()
}
})
// Each forge message wakes a floor whose bridge_tags was silent (design 51 *The floor*). A consumer not there yet is said.
ends := &forgeWakes{logf: logger.Printf, wake: func(ctx context.Context) {
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeForge, "", "")
}}
hear(forgeStream, "the forge's messages are not heard until the builder starts again", func(m jetstream.Msg) { ends.heard(ctx, m) })
if stopped != nil {
return stopped
// A catalog revision the builder hears wakes a floor whose tags or requirements were silent (design 47 G A-119).
half.floor = func(ctx context.Context) {
(&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeRevision, "", "")
}
// The state engine-reads, new with ADR 0618 (issue 1790's kind): until it is on the bus each statement is kept in memory
// only, and builder_engine_reads answers nothing.
@@ -10,9 +10,8 @@ import (
// (novox/hq ADR 0625 §4, ADR 0634 §1), except the clients of the indexer's and the assigner's states, whose holder names
// are used from there.
const (
gateModule = "mesh-delivery-gate"
checkerModule = "mesh-delivery-checker"
forgeBridgeModule = "mesh-delivery-forge-bridge"
gateModule = "mesh-delivery-gate"
checkerModule = "mesh-delivery-checker"
// indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows.
indexerModule = "mesh-delivery-indexer"
@@ -42,7 +42,6 @@ import (
func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) {
t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard)
t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding)
t.Run("a branch's end is acknowledged once its tagged versions are removed (design 51 Tagged versions)", aBranchsEndIsHeard)
refused := refusingBus{err: errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules")}
if _, err := process.EssentialBucket(context.Background(), refusingBus{refuseOpen: true, err: refused.err}, "builder", "mesh-delivery-indexer_modules", "work"); err == nil ||
!strings.Contains(err.Error(), "grant of mesh-delivery-indexer_modules") {
@@ -295,20 +294,6 @@ func (noVersions) Update(context.Context, string, []byte, uint64) (uint64, error
}
func (noVersions) KeysOf(context.Context, string) ([]string, error) { return nil, nil }
func aBranchsEndIsHeard(t *testing.T) {
// A forge message wakes a floor and is acknowledged; no tagged version is removed on a branch's end (design 47 G §373).
var logs bytes.Buffer
woken := 0
h := &forgeWakes{wake: func(context.Context) { woken++ }, logf: log.New(&logs, "", 0).Printf}
open := &wakeMsg{subject: "mesh.mod.mesh-delivery-forge-bridge.event.forge.o.r.pull", data: []byte(`{"kind":"pull","repository":"o/r","branch":"feat/x","action":"opened"}`)}
h.heard(context.Background(), open)
merged := &wakeMsg{subject: "mesh.mod.mesh-delivery-forge-bridge.event.forge.o.r.merged", data: []byte(`{"kind":"merged","repository":"o/r","branch":"feat/x"}`)}
h.heard(context.Background(), merged)
if !open.acked || !merged.acked || woken != 2 || open.delayed != 0 || merged.delayed != 0 {
t.Errorf("an open pull acked %v, a merge acked %v, floors woken %d times", open.acked, merged.acked, woken)
}
}
// failingVersions is a state versions that cannot be read.
type failingVersions struct{ noVersions }
@@ -340,7 +325,7 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
fw := &versions.FloorWaits{Said: f.floorSaid(context.Background())}
fw.Wait("m/lib", versions.SeatGoPackages, "", versions.Floor{Silent: []string{versions.SourceTags}}, nil, time.Unix(0, 0))
floorKey := meshconditionsclient.Key(meshconditionsclient.ScopeModule, "m/lib", versions.FloorSilent)
if got, err := decodeFinding(st.vals[floorKey]); err != nil || !strings.Contains(got.Summary, "bridge_tags did not answer") {
if got, err := decodeFinding(st.vals[floorKey]); err != nil || !strings.Contains(got.Summary, "git.tags did not answer") {
t.Fatalf("the floor finding = %+v, %v", got, err)
}
fw.Answered("m/lib")
@@ -438,12 +423,12 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) {
delete(st.vals, callKey)
// ADR 0615 §5 (review round 1): a consumer not there yet is family-not-heard naming its stream, cleared when it binds.
notHeard := f.notHeard(context.Background())
unheardKey := heard.Key(builds.BuilderModule, "FORGE")
_ = notHeard.Bind("FORGE", "a branch's end is not heard", func() error { return errors.New("consumer not found") })
if got, err := decodeFinding(st.vals[unheardKey]); err != nil || got.Kind != heard.Kind || !strings.Contains(got.Summary, "does not hear FORGE") {
unheardKey := heard.Key(builds.BuilderModule, "EVENTS")
_ = notHeard.Bind("EVENTS", "a branch's end is not heard", func() error { return errors.New("consumer not found") })
if got, err := decodeFinding(st.vals[unheardKey]); err != nil || got.Kind != heard.Kind || !strings.Contains(got.Summary, "does not hear EVENTS") {
t.Fatalf("a consumer not there: the finding = %+v, %v", got, err)
}
_ = notHeard.Bind("FORGE", "a branch's end is not heard", func() error { return nil })
_ = notHeard.Bind("EVENTS", "a branch's end is not heard", func() error { return nil })
if _, ok := st.vals[unheardKey]; ok {
t.Error("a consumer bound leaves family-not-heard")
}
@@ -12,6 +12,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -19,12 +20,6 @@ import (
"mesh-delivery-builder/internal/versions"
)
// The floor's two module tools (novox/hq design 51 *The floor*); the seat's is `seat:<registry>.versions`.
const (
bridgeTagsTool = "bridge_tags"
requirementsTool = "indexer_requirements"
)
// awaitedVersions is the state versions as versions.Store: bound once the bus holds it (the grants part makes it, issue
// 1790); until then every use is an error, so a trunk build is built unnumbered and said (build.Builder.number).
type awaitedVersions struct {
@@ -103,17 +98,9 @@ func (s awaitedVersions) KeysOf(ctx context.Context, filter string) ([]string, e
}
// floorVia is the floor's three sources as calls of the builder's own account (design 51 *The floor*): the seat's verb
// versions, the forge bridge's bridge_tags and the indexer's indexer_requirements. A call that fails is that source
// versions, the seat git's tags and the requirements the catalog half's entries name (A-119). A call that fails is that source
// silent.
func floorVia(call toolCaller, holder func(seat string) (string, bool)) versions.FloorSources {
sourceTool := func(source, tool string) (string, error) {
seat := versions.SourceSeats[source]
module, ok := holder(seat)
if !ok {
return "", fmt.Errorf("no module is known to hold the seat %s, the source of %s", seat, tool)
}
return module + "." + tool, nil
}
func floorVia(call toolCaller, requirements func(context.Context, string) ([]string, error)) versions.FloorSources {
return versions.FloorSources{
Seat: func(_ context.Context, seat, name string) ([]string, error) {
raw, err := call("seat:"+seat+".versions", map[string]any{"name": name})
@@ -137,39 +124,27 @@ func floorVia(call toolCaller, holder func(seat string) (string, bool)) versions
return out, nil
},
Tags: func(_ context.Context, repository, prefix string) ([]string, error) {
tool, err := sourceTool(versions.SourceTags, bridgeTagsTool)
if err != nil {
return nil, err
// The seat git's verb tags, whichever module holds the seat (design 47 G §7, A-82), page by page.
var out []string
for page := int64(1); ; page++ {
raw, err := call("seat:"+gitseat.Seat+"."+gitseat.VerbTags, gitseat.TagsInput{Repository: repository, Prefix: prefix, Page: page})
if err != nil {
return nil, err
}
var a gitseat.TagsAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat git's tags answered what is no list of tags: %w", err)
}
if a.Error != "" {
return nil, fmt.Errorf("the seat git's tags: %s", a.Error)
}
out = append(out, a.Tags...)
if !a.More {
return out, nil
}
}
raw, err := call(tool, map[string]any{"repository": repository, "prefix": prefix})
if err != nil {
return nil, err
}
var a struct {
Tags []string `json:"tags"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("bridge_tags answered what is no list of tags: %w", err)
}
return a.Tags, nil
},
Requirements: func(_ context.Context, name string) ([]string, error) {
tool, err := sourceTool(versions.SourceRequirements, requirementsTool)
if err != nil {
return nil, err
}
raw, err := call(tool, map[string]any{"package": name})
if err != nil {
return nil, err
}
var a struct {
Requirements []string `json:"requirements"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("indexer_requirements answered what is no list of versions: %w", err)
}
return a.Requirements, nil
},
Requirements: requirements,
}
}