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

Builds: design 47 G §298, §300, §303, §213; lead answer A-217

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:07:19 +01:00
parent a5cafab0e8
commit 12e1fd4403
2 changed files with 130 additions and 0 deletions
@@ -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
}
@@ -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)
}
}