From 59b46be0d23158909fbb3354e54efc5cc12a7815 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 02:32:23 +0100 Subject: [PATCH] Indexer (A-326): a branch take-in keeps no revision with a refused cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trunk's take-in refused a cycle with a build edge, but a branch other than the first kept its revision with one. The branch path now applies the same rule before keeping its entries and says the cycle, as it says heads it could not read. Builds: design 47 G §49, §51, A-326 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/index/branch_take_in.go | 6 +++ .../internal/index/branch_take_in_test.go | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/modules/mesh-delivery-indexer/internal/index/branch_take_in.go b/modules/mesh-delivery-indexer/internal/index/branch_take_in.go index 0f274de0..b1df57d8 100644 --- a/modules/mesh-delivery-indexer/internal/index/branch_take_in.go +++ b/modules/mesh-delivery-indexer/internal/index/branch_take_in.go @@ -4,6 +4,7 @@ import ( "context" "errors" "maps" + "strings" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -51,5 +52,10 @@ func (ix *Indexer) ApplyBranchSource(ctx context.Context, branch string, a meshd if err != nil { return err } + // A cycle with a build edge no running version breaks is refused here as at the trunk's take-in (design 47 G §49, A-326). + if cycle := RefusedCycle(WithEdges(ix.withActs(ctx, entries))); cycle != nil { + ix.logf("catalog graph: the branch %s at source revision %d keeps no revision: its catalog graph holds a cycle with a build edge, which no running version breaks (design 47 G §49): %s; taken in again at its next source-moved", branch, a.Revision, strings.Join(cycle, ", ")) + return nil + } return ix.KeepBranchEntries(ctx, branch, a.Revision, entries) } diff --git a/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go b/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go index 253245a2..64786d5b 100644 --- a/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go +++ b/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "slices" + "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" @@ -73,3 +74,40 @@ func TestABranchOtherThanTheFirstIsTakenInIntoItsOwnRevisionAndTheTrunkIsLeftAlo t.Errorf("the same heads said %d revisions (%v), want one", len(said), err) } } + +// Design 47 G §49, A-326: a branch take-in refuses a cycle with a build edge as the trunk's does: the branch keeps no revision +// at those heads and says why, naming every artifact on the cycle. +func TestABranchTakeInKeepsNoRevisionWithARefusedCycle(t *testing.T) { + ctx := context.Background() + mods := builderModules(t) + for i, m := range mods { + switch m.name { + case "avahi": // avahi is built on mesh-tools (kind 3) ... + mods[i].manifest = strings.Replace(catalogueManifest(t, "avahi"), `"build": {`, `"build": { + "on": [{"arg": "RUNTIME_BASE", "module": "mesh-tools", "artifact": "tools"}],`, 1) + case "mesh-tools": // ... and mesh-tools' artifact needs avahi's (kind 2): no build seat breaks the cycle. + mods[i].manifest = strings.Replace(m.manifest, `"name": "tools",`, `"name": "tools", "needs": ["avahi:tools"],`, 1) + } + } + ix, _, merged := newFormIndexerUnmerged(t, mods...) + ix.Revisions = newMemObjects(0) + ix.Graph = newMem() + var logs strings.Builder + ix.Logf = func(format string, args ...any) { fmt.Fprintf(&logs, format+"\n", args...) } + ix.Message = func(context.Context, string, string, int) ([]byte, error) { return json.Marshal(merged) } + var said []meshcatalog.CatalogRevisionEvent + ix.SayCatalogRevision = func(_ context.Context, e meshcatalog.CatalogRevisionEvent) error { + said = append(said, e) + return nil + } + answer := meshdelivery.SourceAnswer{Branch: "beta", Revision: 9, Repositories: []meshdelivery.SourceHead{{Repository: catalogueRepo, Head: merged.Merge, Trunk: "beta"}}} + if err := ix.ApplyBranchSource(ctx, "beta", answer); err != nil { + t.Fatal(err) + } + if _, _, err := ReadRevision(ctx, ix.Revisions, BranchObject("beta", 1)); err == nil || len(said) != 0 { + t.Errorf("the branch kept a revision with a refused cycle (said %+v)", said) + } + if want := "mesh-tools:tools, avahi:tools, mesh-tools:tools"; !strings.Contains(logs.String(), want) { + t.Errorf("the journal %q does not say the cycle %s", logs.String(), want) + } +}