DLV-C5: the checker asks the seat mesh-config's verb renames for every change that brings or drops a renamed-from

Design 32 (A config value renamed): the checker invokes seat:mesh-config.renames, never the module. A
change bringing renamed-from to a holder that serves no renames (nothing answers the optional verb) is
refused naming it; dropping a renamed-from while its window is open is refused naming the nodes holding it
open; a check that cannot ask fails, saying so. The pull request's trunk and head manifests are read once
for this and for the series changes (manifestPair). Rows in TestTheIndexersJudgeJudgesTheChangesManifests
with the verb faked from the seat's definition (its fixture's answer): not served, served, open on g14,
closed, an ask that fails, no seat to ask; the four refusals failed first. Manifest row for the invokes.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:32:41 +01:00
parent 890b8f36e9
commit 30ee47caa6
10 changed files with 250 additions and 35 deletions
@@ -25,6 +25,7 @@ import (
// judgeDeps are the parts the judge of a check is made of.
type judgeDeps struct {
asks check.Asks
renames check.Renames
tools *buildrun.ToolchainSet
trunks *check.Trunks
kept check.KV
@@ -155,7 +156,7 @@ func newJudge(d judgeDeps) check.Judge {
nodes = d.nodes
}
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Logf: d.logf}
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, Logf: d.logf}
if d.nodes != nil && d.builds != nil && d.repos != nil {
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
}
@@ -263,6 +263,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
announce: announce, compositions: compositions, tags: tags,
nodes: openNodeNames(ctx, js, logger.Printf),
predefined: keptPredefined{predefined},
renames: seatRenames{call: caller.Call},
})
ch.Announce = announce
tryer.Judge = ch.Judge
@@ -0,0 +1,36 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/nats-io/nats.go"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"mesh-delivery-checker/internal/check"
)
// seatRenames asks the seat mesh-config's verb renames as the checker's own account (its `invokes`
// seat:mesh-config.renames; design 32). A call nobody answers is a holder serving no renames (the verb is optional), said
// as check.ErrRenamesNotServed with the bus's words; any other error is an ask that could not be made.
type seatRenames struct {
call func(key string, body any) (json.RawMessage, error)
}
func (s seatRenames) Renames(_ context.Context, module string) (meshconfig.RenamesAnswer, error) {
raw, err := s.call(check.RenamesSeat, meshconfig.RenamesInput{Module: module})
if errors.Is(err, nats.ErrNoResponders) {
return meshconfig.RenamesAnswer{}, fmt.Errorf("%w (nothing answers it: %v)", check.ErrRenamesNotServed, err)
}
if err != nil {
return meshconfig.RenamesAnswer{}, err
}
var a meshconfig.RenamesAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return meshconfig.RenamesAnswer{}, fmt.Errorf("%s answered what is not its answer: %w", check.RenamesSeat, err)
}
return a, nil
}
+1
View File
@@ -12,6 +12,7 @@ require git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-2
require (
git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000
@@ -19,6 +19,7 @@ import (
"unicode/utf8"
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
@@ -1037,7 +1038,18 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
trunkCycle bool
// series is the series of web's package the change raises it to, and the words its status line names (DLV-C4).
series, seriesWords string
// renames is what the change does to web's renamed-from ("bring", "drop") and what the seat mesh-config's verb
// renames answers ("served", "open" on g14, "not served", "unasked" with no seat to ask, "error"); DLV-C5.
renames, seat string
}{
{name: "a rename brought, the verb not served", renames: "bring", seat: "not served",
fails: "web (novox/mesh-delivery#7) renames new-key from old-key: refused, the holder of the seat mesh-config does not serve its verb renames"},
{name: "a rename brought, the verb served", renames: "bring", seat: "served"},
{name: "a renamed-from dropped while its window is open", renames: "drop", seat: "open",
fails: "web (novox/mesh-delivery#7) drops renamed-from old-key of new-key while its rename window is open on g14"},
{name: "a renamed-from dropped once its window closed", renames: "drop", seat: "served"},
{name: "a rename the check cannot ask of", renames: "bring", seat: "error", fails: "the check cannot ask seat:mesh-config.renames: the bus said no"},
{name: "a rename with no seat to ask", renames: "drop", seat: "unasked", fails: "this checker cannot ask seat:mesh-config.renames"},
{name: "accepted"},
{name: "a package series raised", series: "0.2", seriesWords: "web/pkg series 0.1 → 0.2"},
{name: "a package series kept", series: "0.1"},
@@ -1088,6 +1100,28 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web", Build: pkg("")},
web: {Module: "web", Build: pkg(row.series)}}
}
if row.renames != "" {
renamed := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString, RenamedFrom: "old-key"}}
plain := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString}}
trunkWeb, headWeb := catalogue.Manifest{Module: "web", Settings: plain}, catalogue.Manifest{Module: "web", Settings: renamed}
if row.renames == "drop" {
trunkWeb, headWeb = headWeb, trunkWeb
}
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): trunkWeb, web: headWeb}
// The verb faked from its definition: the answer of the seat's fixture (mesh-contracts seats/mesh-config).
fake := fakeRenames{}
switch row.seat {
case "not served":
fake.err = fmt.Errorf("%w (nothing answers it)", ErrRenamesNotServed)
case "error":
fake.err = errors.New("the bus said no")
case "open":
fake.answer = meshconfig.RenamesAnswer{Renames: []meshconfig.RenamesAnswerRenamesItem{{Module: "web", From: "old-key", To: "new-key", At: "2026-11-04T12:00:00Z", Open: []string{"g14"}}}}
}
if row.seat != "unasked" {
b.ex.Renames = fake
}
}
var judged []delivery.ComposeJob
answer := b.asks.Answer
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
@@ -1134,3 +1168,11 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
t.Error("which merge changes the judge")
}
}
// fakeRenames is the seat mesh-config's verb renames as its definition answers it.
type fakeRenames struct {
answer meshconfig.RenamesAnswer
err error
}
func (f fakeRenames) Renames(context.Context, string) (meshconfig.RenamesAnswer, error) { return f.answer, f.err }
@@ -128,6 +128,9 @@ type Examiner struct {
// Manifests are the indexer's manifest digests by module: the catalogue a change's manifests are judged in by the
// indexer's judge (manifests_judge.go, novox/hq ADR 0578 §2); nil judges no manifest.
Manifests TrunkManifests
// Renames asks the seat mesh-config's verb renames for a change that brings or drops a `renamed-from` (renames.go,
// design 32); nil fails such a change, saying it cannot ask.
Renames Renames
// Read reads a kept manifest by its digest: the change's module graph is worked out from them (graph_cycle.go, novox/hq
// ADR 0603 §2); nil works out no graph.
Read ManifestReader
@@ -270,7 +273,7 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
// 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02).
tests := st.judgeAll(ctx)
st.seriesChanges(ctx) // DLV-C4: named in the verdict's line, whatever the verdict
if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx))); v != nil {
if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx), st.judgeRenames(ctx))); v != nil {
return st.finish(ctx, *v), nil
}
return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil
@@ -0,0 +1,127 @@
package check
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// A config value renamed (design 32 *A config value renamed*, DLV-C5): the checker asks the seat mesh-config's verb
// renames, never the module, for every change that brings `renamed-from` or drops one. A holder that does not serve the
// verb refuses `renamed-from`, naming that; while a rename's window is open, dropping its `renamed-from` is refused,
// naming the nodes holding it open; a check that cannot ask fails, saying so.
// RenamesSeat is the verb the checker asks, as its manifest's `invokes` names it.
var RenamesSeat = "seat:" + meshconfig.Seat + "." + meshconfig.VerbRenames
// ErrRenamesNotServed is the answer of Renames when the seat's holder serves no verb renames.
var ErrRenamesNotServed = errors.New("the holder of the seat " + meshconfig.Seat + " does not serve its verb " + meshconfig.VerbRenames)
// Renames asks the seat mesh-config's verb renames for a module's open renames.
type Renames interface {
Renames(ctx context.Context, module string) (meshconfig.RenamesAnswer, error)
}
// renameChange is what a pull request's head does to a module's `renamed-from` against the trunk: the declarations it
// brings (key from old name) and the ones it drops (key, old name).
type renameChange struct {
brought, dropped map[string]string
}
// renamesOf compares a module's config declarations at the trunk and at the head.
func renamesOf(was, now catalogue.Manifest) renameChange {
rc := renameChange{brought: map[string]string{}, dropped: map[string]string{}}
for key, d := range now.Settings {
if d.RenamedFrom != "" && was.Settings[key].RenamedFrom != d.RenamedFrom {
rc.brought[key] = d.RenamedFrom
}
}
for key, d := range was.Settings {
if d.RenamedFrom != "" && now.Settings[key].RenamedFrom != d.RenamedFrom {
rc.dropped[key] = d.RenamedFrom
}
}
return rc
}
// judgeRenames fails the check for a rename the seat refuses, a window a change would drop, or an ask that cannot be made.
func (st *state) judgeRenames(ctx context.Context) *delivery.Verdict {
if st.c.Try != nil {
return nil
}
var out *delivery.Verdict
failed := func(words string) {
f := delivery.FailedFirst{Machine: meshconfig.Seat, Words: words}
if out == nil {
out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f}
return
}
addFailure(out, f)
}
for _, p := range st.c.Pulls {
for _, module := range p.Modules {
was, now, ok, err := st.manifestPair(ctx, p, module)
if err != nil {
failed(fmt.Sprintf("%s (%s): its config renames cannot be judged: %v", module, pullKey(p), err))
continue
}
if !ok {
was = catalogue.Manifest{} // a module new on the pull request brings every rename it declares
if now, ok = st.headManifest(ctx, p, module); !ok {
continue
}
}
rc := renamesOf(was, now)
if len(rc.brought) == 0 && len(rc.dropped) == 0 {
continue
}
if st.j.Renames == nil {
failed(fmt.Sprintf("%s (%s) changes a renamed-from, and this checker cannot ask %s", module, pullKey(p), RenamesSeat))
continue
}
answer, err := st.j.Renames.Renames(ctx, module)
switch {
case errors.Is(err, ErrRenamesNotServed):
for _, key := range slices.Sorted(maps.Keys(rc.brought)) {
failed(fmt.Sprintf("%s (%s) renames %s from %s: refused, %v, so the value cannot be moved", module, pullKey(p), key, rc.brought[key], err))
}
for _, key := range slices.Sorted(maps.Keys(rc.dropped)) {
failed(fmt.Sprintf("%s (%s) drops renamed-from %s of %s: refused, %v, so whether its window is open is not known", module, pullKey(p), rc.dropped[key], key, err))
}
continue
case err != nil:
failed(fmt.Sprintf("%s (%s) changes a renamed-from, and the check cannot ask %s: %v", module, pullKey(p), RenamesSeat, err))
continue
}
for _, key := range slices.Sorted(maps.Keys(rc.dropped)) {
for _, r := range answer.Renames {
if r.Module == module && r.From == rc.dropped[key] && r.To == key && len(r.Open) > 0 {
open := slices.Sorted(slices.Values(r.Open))
failed(fmt.Sprintf("%s (%s) drops renamed-from %s of %s while its rename window is open on %s", module, pullKey(p), r.From, key, strings.Join(open, ", ")))
}
}
}
st.c.note(fmt.Sprintf("%s.%s answered for %s (%s)", meshconfig.Seat, meshconfig.VerbRenames, module, pullKey(p)))
}
}
return out
}
// headManifest is a module's manifest at the pull request's head, new on it: by its candidate's digest, else the forge's.
func (st *state) headManifest(ctx context.Context, p Pull, module string) (catalogue.Manifest, bool) {
if digest := p.Fps[module].Manifest; digest != "" && st.j.Read != nil {
if st.j.Manifests != nil && st.j.Manifests.ManifestDigests()[module] == digest {
return catalogue.Manifest{}, false // unchanged
}
m, err := st.j.Read.Manifest(ctx, digest)
return m, err == nil
}
return catalogue.Manifest{}, false
}
@@ -16,45 +16,17 @@ import (
// seriesChanges names, on the case per pull request, each change of an artifact's series the pull request makes.
func (st *state) seriesChanges(ctx context.Context) {
if st.j.Read == nil {
return
}
var trunk map[string]string
if st.j.Manifests != nil {
trunk = st.j.Manifests.ManifestDigests()
}
for _, p := range st.c.Pulls {
var words []string
for _, module := range p.Modules {
if trunk[module] == "" {
continue // new on this pull request: no series is changed
}
was, err := st.j.Read.Manifest(ctx, trunk[module])
was, now, ok, err := st.manifestPair(ctx, p, module)
if err != nil {
words = append(words, fmt.Sprintf("%s: the trunk's series not read (%v)", module, err))
words = append(words, fmt.Sprintf("%s: its series not compared (%v)", module, err))
continue
}
var now catalogue.Manifest
switch digest := p.Fps[module].Manifest; {
case digest == trunk[module]:
continue
case digest != "":
if now, err = st.j.Read.Manifest(ctx, digest); err != nil {
words = append(words, fmt.Sprintf("%s: the head's series not read (%v)", module, err))
continue
}
case st.j.Forge != nil:
var found bool
if now, _, found, err = st.manifestAtHead(ctx, p, module); err != nil {
words = append(words, fmt.Sprintf("%s: the head's series not read (%v)", module, err))
continue
} else if !found {
continue
}
default:
continue
if ok {
words = append(words, seriesMoved(module, was, now)...)
}
words = append(words, seriesMoved(module, was, now)...)
}
if len(words) > 0 {
st.c.series(pullKey(p), words)
@@ -62,6 +34,37 @@ func (st *state) seriesChanges(ctx context.Context) {
}
}
// manifestPair is a touched module's trunk manifest and its manifest at the pull request's head: the head's by the digest
// its candidate names, else read from the forge at the head. ok is false when the module is new on the pull request,
// unchanged, or not read here.
func (st *state) manifestPair(ctx context.Context, p Pull, module string) (was, now catalogue.Manifest, ok bool, err error) {
if st.j.Read == nil || st.j.Manifests == nil {
return was, now, false, nil
}
trunk := st.j.Manifests.ManifestDigests()[module]
digest := p.Fps[module].Manifest
if trunk == "" || digest == trunk {
return was, now, false, nil
}
if was, err = st.j.Read.Manifest(ctx, trunk); err != nil {
return was, now, false, fmt.Errorf("the trunk's manifest not read: %w", err)
}
switch {
case digest != "":
if now, err = st.j.Read.Manifest(ctx, digest); err != nil {
return was, now, false, fmt.Errorf("the head's manifest not read: %w", err)
}
case st.j.Forge != nil:
var found bool
if now, _, found, err = st.manifestAtHead(ctx, p, module); err != nil || !found {
return was, now, false, err
}
default:
return was, now, false, nil
}
return was, now, true, nil
}
// seriesMoved are the artifacts of module whose series the head's manifest changes from the trunk's.
func seriesMoved(module string, was, now catalogue.Manifest) []string {
if was.Build == nil || now.Build == nil {
@@ -123,7 +123,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
"seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
"seat:mesh-config.renames", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
}
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
@@ -79,6 +79,7 @@
"mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_packages",
"mesh-delivery-indexer.indexer_read_predefined",
"seat:mesh-config.renames",
"seat:node-engine.compose-candidate"
],