AS3: a placement's branch and build, set by a proposal, bound by the ask, and its build a held put

A proposal sets a module's watched branch and build (a re-place keeps them), the changes digest an ask binds to
carries both, and a placement's build is a held put of that build, released when the placement drops it while a
person's put stays. Taken over from 6bc6fe53 and b8e50fff.

Builds: design 47 G §14, §78, §114, §360, §361
Tests: TestAssignments (place change branch, build, digest rows), aPlacementsBuildIsPut. Test 77 (§79) covered by the put row.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:57:53 +01:00
parent 91a9c41290
commit b1fcabcebe
6 changed files with 187 additions and 3 deletions
@@ -272,6 +272,7 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) {
t.Error("an answer to an ask taken away was taken")
}
t.Run("a put of the bus acts on the operator's warrant alone", putOfTheBus)
t.Run("a placement's build is put held, and released when the placement names it no more", aPlacementsBuildIsPut)
// ADR 0614 §4: the rules read what each node's engine states it reads from its record in `<node>.node`; a node with no
// record, or one that states nothing, is read as stating nothing (the rules then read the baseline).
t.Run("the rules read what each node's engine reads", func(t *testing.T) {
@@ -467,6 +468,63 @@ func (b *fakeBus) Emit(eventType string, body any) error {
return nil
}
// Design 47 line 912 and line 914 ("a placement's `build` delivers the earlier build"; ADR 0666 §24, B-K1): a placed
// module's build becomes a held put of that build, which then goes a put's steps; a placement naming no build any more
// releases the put it made, never a person's.
func aPlacementsBuildIsPut(t *testing.T) {
ctx := context.Background()
putsKV := &memKV{values: map[string][]byte{}}
p := newPasser(kvSet{buckets.Puts: putsKV}, time.Date(2026, 11, 10, 18, 0, 0, 0, time.UTC))
earlier := meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64)
place := func(build string) {
values, err := mmac.AssignmentsValues(mma.Assignments{Node: "g14", Modules: []mma.PlacedModule{{Module: "web", Build: build}, {Module: "db"}}})
if err != nil {
t.Fatal(err)
}
for key, raw := range values {
p.w.putEntry(plc.AssignmentsBucket, key, raw, uint64(len(raw)), time.Now())
}
}
read := func() (puts.Put, bool) {
raw, ok := putsKV.values["g14.web"]
if !ok {
return puts.Put{}, false
}
got, err := puts.Decode(raw)
if err != nil {
t.Fatal(err)
}
return got, true
}
place(earlier)
p.placementPuts(ctx)
got, ok := read()
if !ok || got.FP != earlier || !got.Hold || got.By != placementBy || got.Step != puts.Asked {
t.Fatalf("the placement's build made the put %+v (there %v)", got, ok)
}
if _, there := putsKV.values["g14.db"]; there {
t.Error("a placed module naming no build was put")
}
p.placementPuts(ctx) // again: the same build makes nothing new
if again, _ := read(); !again.At.Equal(got.At) || again.Step != puts.Asked {
t.Errorf("a second pass made the put again: %+v", again)
}
place("")
p.placementPuts(ctx)
if got, _ := read(); got.Step != puts.Released || !strings.Contains(got.Words, placementBy) {
t.Errorf("the placement naming no build left the put %+v", got)
}
// A person's put stands while the placement names no build.
person := puts.Put{FP: earlier, Hold: true, Why: "a try", By: "lead@shanks", At: time.Now()}.Said(time.Now(), puts.Holding, "held")
raw, _ := puts.Encode(person)
putsKV.values["g14.web"] = raw
p.w.put(buckets.Puts, "g14.web", raw)
p.placementPuts(ctx)
if got, _ := read(); got.Step != puts.Holding || got.By != "lead@shanks" {
t.Errorf("a person's put was released by the placement: %+v", got)
}
}
// ADR 0546, the first put (2026-10-25, want.novox.nats): the tool writes the put; a pass waits for the composition and grant
// the controller's take-in writes; once they are there it asks the operator with a yes that approves; only on that warrant does it have
// the streams snapshotted, then writes the want held and records the warrant; the have running it healthy holds it; a release
@@ -0,0 +1,95 @@
package main
import (
"context"
"fmt"
"sort"
"mesh-delivery-assigner/internal/buckets"
"mesh-delivery-assigner/internal/puts"
)
// placementBy is who a put the placement's build makes names as its asker: the placement, whose proposal the operator
// approved, is the warrant (design 47 *Going back is one act*; ADR 0666 §24).
const placementBy = "the placement"
// placementPuts makes each placed module's `build` a held put of that build, and releases a put the placement made once its
// placement names that build no more (design 47 line 912: "a return to an earlier build is one value in the placement, the
// module's `build` in its placement key, set by a placement proposal"; ADR 0666 §24; B-K1). The put then goes its own steps
// (the builder's build, the composition, an ask where the module's policy needs one, the want, holding) and is read with
// `assigner_puts` and released with `assigner_release` like any other; a placement's build the builder never built is refused
// by its build step, said in the put's words. A put asked by a person stands until the placement names another build.
func (p *passer) placementPuts(ctx context.Context) {
kv := p.kvs[buckets.Puts]
if kv == nil {
return
}
now := p.clock().UTC()
named := map[string]string{} // put key -> the build its placement names
for node, a := range p.w.assignments() {
for _, m := range a.Modules {
if m.Build == "" {
continue
}
if key, err := puts.Key(node, m.Module); err == nil {
named[key] = m.Build
}
}
}
held := p.w.read(buckets.Puts)
keys := make([]string, 0, len(named)+len(held))
for k := range named {
keys = append(keys, k)
}
for k := range held {
if _, there := named[k]; !there {
keys = append(keys, k)
}
}
sort.Strings(keys)
for _, key := range keys {
build, names := named[key]
var before *puts.Put
if raw, ok := held[key]; ok {
if got, err := puts.Decode(raw); err == nil {
before = &got
}
}
var next puts.Put
switch {
case names && before != nil && before.FP == build:
continue // made already, or a person's put of the same build: it goes its own steps, ended or not
case names:
words := fmt.Sprintf("asked by %s: it names build %s of %s", placementBy, shortFP(build), key)
if before != nil && !puts.Ended(before.Step) {
words += fmt.Sprintf("; it replaces the put of %s, which stood at %s", shortFP(before.FP), before.Step)
}
next = puts.Put{FP: build, Hold: true, Why: "the placement names this build (ADR 0666 §24)", By: placementBy, At: now}.Said(now, puts.Asked, words)
case before != nil && before.By == placementBy && !puts.Ended(before.Step):
next = before.Said(now, puts.Released, fmt.Sprintf("released by %s: it names no build of its own any more; the assigner's rules write the want again", placementBy))
default:
continue
}
data, err := puts.Encode(next)
if err != nil {
p.logf("the placement's put %s is not written: %v", key, err)
continue
}
if before == nil {
_, err = kv.Create(ctx, key, data)
} else if e, gerr := kv.Get(ctx, key); gerr != nil {
err = gerr
} else {
_, err = kv.Update(ctx, key, data, e.Revision())
}
if err != nil {
p.logf("the placement's put %s could not be written, and is written at the next pass: %v", key, err)
continue
}
p.w.put(buckets.Puts, key, data)
p.logf("put %s: %s: %s", key, next.Step, next.Words)
if p.acts != nil {
p.acts.say(key, next)
}
}
}
@@ -105,6 +105,7 @@ func (p *passer) puts(ctx context.Context) {
if p.acts == nil {
return
}
p.placementPuts(ctx)
all := p.putsByModule()
if len(all) == 0 {
return
@@ -129,6 +129,24 @@ func TestAssignments(t *testing.T) {
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
t.Error("laying changed the current assignments")
}
// Design 47 line 1051 (the Choices table: a module's watched branch and its build in the placement key, set by a
// proposal): a place change sets them, and a re-place naming none keeps the module's.
onBeta, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Branch: "beta", Build: "b1"}})
again, g := LayChanges(onBeta, []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Ports: map[string]int64{"80": 8081}}})
if m := onBeta["g14"].Modules[0]; len(f) != 0 || m.Branch != "beta" || m.Build != "b1" {
t.Errorf("a place change's branch and build: %+v, %v", m, f)
}
if m := again["g14"].Modules[0]; len(g) != 0 || m.Branch != "beta" || m.Build != "b1" || m.Ports["80"] != 8081 {
t.Errorf("a re-place dropped the branch and build: %+v, %v", m, g)
}
if ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Branch: "beta"}}}) ==
ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh"}}}) {
t.Error("an ask bound to a place change's digest does not bind its branch")
}
if ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh", Build: "b1"}}}) ==
ChangesDigest(seat.Proposal{Changes: []seat.ProposalChange{{Op: client.OpPlace, Node: "g14", Module: "zsh"}}}) {
t.Error("an ask bound to a place change's digest does not bind its build (design 47 G 360)")
}
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
t.Errorf("a node with no key: %+v, %v", fresh, f)
}
@@ -29,6 +29,8 @@ type placedForm struct {
HasLayer bool `json:"has-layer,omitempty"`
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
Values map[string]any `json:"values,omitempty"`
Branch string `json:"branch,omitempty"`
Build string `json:"build,omitempty"`
}
type holdingForm struct {
@@ -110,6 +112,8 @@ type changeForm struct {
Whole *assignmentsForm `json:"whole,omitempty"`
Decided *decidedForm `json:"decided,omitempty"`
Keys *joiningForm `json:"keys,omitempty"`
Branch string `json:"branch,omitempty"`
Build string `json:"build,omitempty"`
}
func providerOf(p seat.Provider) providerForm { return providerForm{Node: p.Node, Module: p.Module} }
@@ -150,7 +154,7 @@ func assignmentsOf(p seat.Assignments) assignmentsForm {
}
for _, m := range p.Modules {
out.Modules = append(out.Modules, placedForm{Module: m.Module, Ports: m.Ports, Bindings: providersOf(m.Bindings),
MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values})
MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values, Branch: m.Branch, Build: m.Build})
}
for _, h := range p.Holds {
out.Holds = append(out.Holds, holdingForm{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
@@ -161,7 +165,7 @@ func assignmentsOf(p seat.Assignments) assignmentsForm {
func changeOf(c seat.ProposalChange) changeForm {
out := changeForm{Op: c.Op, Node: c.Node, Module: c.Module, Ports: c.Ports, Bindings: providersOf(c.Bindings), Provision: c.Provision,
Claim: c.Claim, Scope: c.Scope, Kind: c.Kind, Site: c.Site, Field: c.Field, MeshWide: c.MeshWide, Layer: c.Layer,
MeshValues: c.MeshWideValues, Values: c.Values}
MeshValues: c.MeshWideValues, Values: c.Values, Branch: c.Branch, Build: c.Build}
if c.Provider != nil {
p := providerOf(*c.Provider)
out.Provider = &p
@@ -148,11 +148,19 @@ func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChan
}
continue
case client.OpPlace:
m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings}
// A module's watched branch and its build are in the placement key, set by a proposal (design 47 line 1051,
// the Choices table's row): the change's when it names them, else kept from the module placed there.
m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings, Branch: c.Branch, Build: c.Build}
if at >= 0 {
o := p.Modules[at]
m.MeshWide, m.HasMeshWide, m.Layer, m.HasLayer = o.MeshWide, o.HasMeshWide, o.Layer, o.HasLayer
m.MeshWideValues, m.Values = o.MeshWideValues, o.Values
if m.Branch == "" {
m.Branch = o.Branch
}
if m.Build == "" {
m.Build = o.Build
}
p.Modules[at] = m
} else {
p.Modules = append(p.Modules, m)