From 12e1fd440376b40a92655bd89e0ff2a18b27ab01 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:07:19 +0100 Subject: [PATCH] AS17 (a, b): a revert is a want going back to an earlier fingerprint by the assignment revisions' numbers; a kept data's form change is read from the entries' manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §298, §300, §303, §213; lead answer A-217 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/assigngraph/revert.go | 81 +++++++++++++++++++ .../internal/assigngraph/revert_test.go | 49 +++++++++++ 2 files changed, 130 insertions(+) create mode 100644 modules/mesh-delivery-assigner/internal/assigngraph/revert.go create mode 100644 modules/mesh-delivery-assigner/internal/assigngraph/revert_test.go diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/revert.go b/modules/mesh-delivery-assigner/internal/assigngraph/revert.go new file mode 100644 index 00000000..4362a850 --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assigngraph/revert.go @@ -0,0 +1,81 @@ +package assigngraph + +import ( + "context" + "encoding/json" + "fmt" + "slices" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Reverted says a want of module on node at wantFp is a revert (design 47 G §298, §300, A-217): the module had wantFp in an earlier +// assignment graph revision than the one it now runs from, by the revisions' own numbers. The revision it runs from is the first kept +// that lists runFp for the module on the node; a want is a revert when it was listed before that, and never listed since. +func Reverted(ctx context.Context, o Objects, node, module, runFp, wantFp string) (bool, error) { + if runFp == "" || wantFp == "" || runFp == wantFp { + return false, nil + } + names, err := o.Names(ctx) + if err != nil { + return false, fmt.Errorf("the objects %s: %w", RevisionsObjects, err) + } + var numbers []int64 + for _, n := range names { + if r, ok := trunkOf(n); ok { + numbers = append(numbers, r) + } + } + slices.Sort(numbers) + var ranFrom, lastWanted int64 + for _, r := range numbers { + rev, err := ReadRevision(ctx, o, r) + if err != nil { + return false, err + } + for _, ref := range rev.Entries { + if ref.Node != node || ref.Module != module { + continue + } + e, err := ReadEntry(ctx, o, ref.Digest) + if err != nil { + return false, err + } + switch e.Fp { + case runFp: + if ranFrom == 0 { + ranFrom = r + } + case wantFp: + lastWanted = r + } + } + } + return ranFrom != 0 && lastWanted != 0 && lastWanted < ranFrom, nil +} + +// FormChanges are the kept data whose `form` differs between the running entry's manifest and the want's (design 47 G §295 to §298, +// A-217): the entry's manifest whole is read as a manifest and compared by catalogue.FormChanges, so a kept data's form is read from +// the entry's kept declarations (catalogue.KeptDeclarations), never from a field of the want. +func FormChanges(running, applied meshcatalog.CatalogEntry) ([]catalogue.FormChange, error) { + before, err := manifestOf(running) + if err != nil { + return nil, fmt.Errorf("the running manifest of %s: %w", running.Module, err) + } + after, err := manifestOf(applied) + if err != nil { + return nil, fmt.Errorf("the manifest of %s: %w", applied.Module, err) + } + return catalogue.FormChanges(before, after), nil +} + +func manifestOf(e meshcatalog.CatalogEntry) (catalogue.Manifest, error) { + var m catalogue.Manifest + raw, err := json.Marshal(e.Manifest) + if err != nil { + return m, err + } + err = json.Unmarshal(raw, &m) + return m, err +} diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/revert_test.go b/modules/mesh-delivery-assigner/internal/assigngraph/revert_test.go new file mode 100644 index 00000000..a824bffa --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assigngraph/revert_test.go @@ -0,0 +1,49 @@ +package assigngraph + +import ( + "context" + mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + "testing" + "time" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" +) + +// Design 47 G §298, §300, A-217 (AS17): a want is a revert when the module had its fingerprint in an earlier assignment graph revision than +// the one it now runs from; a kept data whose form changed is read from the entries' manifests. +func TestAWantGoingBackToAnEarlierFingerprintIsARevert(t *testing.T) { + ctx := context.Background() + o := memObjects{} + at := time.Now() + for _, fp := range []string{"src2:a", "src2:b", "src2:c"} { + in := Inputs{Halves: map[string]meshcatalogclient.Half{"main": half("main", 1, meshcatalog.CatalogEntry{Module: "web", Fp: fp})}, + Placements: map[string]mma.Assignments{"ace": {Node: "ace", Modules: []mma.PlacedModule{{Module: "web"}}}}, FirstBranch: func(string) string { return "main" }} + if _, _, err := Keep(ctx, o, Compute(in), "test", at); err != nil { + t.Fatal(err) + } + } + for _, c := range []struct { + run, want string + revert bool + }{{"src2:c", "src2:a", true}, {"src2:c", "src2:b", true}, {"src2:a", "src2:c", false}, {"src2:b", "src2:b", false}, {"src2:c", "src2:zz", false}} { + if got, err := Reverted(ctx, o, "ace", "web", c.run, c.want); err != nil || got != c.revert { + t.Errorf("runs %s, wanted %s: revert %v %v, want %v", c.run, c.want, got, err, c.revert) + } + } +} + +func TestAKeptDatasFormIsReadFromTheEntrysManifest(t *testing.T) { + entry := func(form int) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: "web", Manifest: map[string]any{"module": "web", + "state": []any{map[string]any{"name": "sessions", "form": form}}}} + } + same, err := FormChanges(entry(1), entry(1)) + if err != nil || len(same) != 0 { + t.Fatalf("same form: %v %v", same, err) + } + got, err := FormChanges(entry(1), entry(2)) + if err != nil || len(got) != 1 || got[0].Name != "sessions" { + t.Errorf("changed form: %+v %v", got, err) + } +}