VW2: the assigner's difference fills Tiers and each change's after

assigngraph.Compare (verbs.go:80, new order at verbs.go:112) orders the changes by the edges of their entries: each change
names the changes it is ordered after with the edge's kind (meshwants.Change.after, seat.json, regenerated), and Tiers holds
them in that order, so the view orders nothing itself.

Builds: design 47 G §512, §186, §378, A-242
This commit is contained in:
2026-11-12 06:28:02 +01:00
parent da8384d10e
commit c8d842b924
5 changed files with 182 additions and 0 deletions
@@ -174,3 +174,56 @@ func TestARevisionOf120ModulesOnFourNodesFitsOneValue(t *testing.T) {
t.Errorf("%d entries in %d bytes", len(c.Revision.Entries), len(raw))
}
}
// Design 47 G §512, §186, A-242: the difference orders its changes itself: each change names the changes it is ordered after
// with the kind of the edge, and Tiers holds the changes in that order, an acting part and a provider before what they act on.
func TestTheDifferenceFillsItsTiersAndEachChangesAfter(t *testing.T) {
ctx := context.Background()
build := func(ne, pg, web string) Computed {
return Compute(Inputs{
Halves: map[string]meshcatalogclient.Half{"main": half("main", 7,
meshcatalog.CatalogEntry{Module: "node-engine", Fp: ne, Acts: []meshcatalog.ActingRule{{Seat: "node-engine", Step: "compose", On: "every"}}},
meshcatalog.CatalogEntry{Module: "postgres", Fp: pg, Provides: []string{"database"}},
meshcatalog.CatalogEntry{Module: "web", Fp: web, Requires: []string{"database"}},
)},
Placements: map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "node-engine"}, {Module: "postgres"},
{Module: "web", Bindings: map[string]mma.Provider{"database": {Node: "anchor", Module: "postgres"}}}}}},
FirstBranch: func(string) string { return "main" },
})
}
o := memObjects{}
at := time.Date(2026, 11, 12, 1, 0, 0, 0, time.UTC)
if _, _, err := Keep(ctx, o, build("src2:e1", "src2:p1", "src2:w1"), "catalog-revision", at); err != nil {
t.Fatal(err)
}
if _, _, err := Keep(ctx, o, build("src2:e2", "src2:p2", "src2:w2"), "catalog-revision", at); err != nil {
t.Fatal(err)
}
d, err := Difference(ctx, o, meshwants.DifferenceInput{From: 1, To: 2})
if err != nil || len(d.Changes) != 3 {
t.Fatalf("difference %+v, %v", d, err)
}
after := map[string]string{}
for _, c := range d.Changes {
var a []string
for _, x := range c.After {
a = append(a, x.Kind+":"+x.Node+"/"+x.Module)
}
after[c.Module] = strings.Join(a, ",")
}
if after["node-engine"] != "" || after["postgres"] != "acting:anchor/node-engine" ||
after["web"] != "acting:anchor/node-engine,provisioning:anchor/postgres" {
t.Errorf("after = %v", after)
}
var tiers []string
for _, tier := range d.Tiers {
var ms []string
for _, r := range tier {
ms = append(ms, r.Module)
}
tiers = append(tiers, strings.Join(ms, ","))
}
if strings.Join(tiers, "|") != "node-engine|postgres|web" {
t.Errorf("tiers = %v", tiers)
}
}
@@ -6,6 +6,7 @@ import (
"fmt"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
)
@@ -94,6 +95,7 @@ func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevisi
return d, err
}
nodes := map[string]bool{}
edges := map[string][]meshwants.Edge{}
for _, k := range slices.Sorted(maps.Keys(union(was, now))) {
a, hadA := was[k]
b, hasB := now[k]
@@ -110,11 +112,95 @@ func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevisi
c.Edges = moved(a.Edges, b.Edges)
d.Changes = append(d.Changes, c)
nodes[c.Node] = true
if hasB {
edges[c.Node+"/"+c.Module] = b.Edges
} else {
edges[c.Node+"/"+c.Module] = a.Edges
}
}
d.Nodes = slices.Sorted(maps.Keys(nodes))
order(&d, edges, digests(from, to))
return d, nil
}
// digests are the entry digests of the two revisions by node and module, the newer's winning.
func digests(from, to meshwants.AssignmentRevision) map[string]string {
out := map[string]string{}
for _, r := range []meshwants.AssignmentRevision{from, to} {
for _, ref := range r.Entries {
out[ref.Node+"/"+ref.Module] = ref.Digest
}
}
return out
}
// order fills each change's After and the difference's Tiers (design 47 G §512, §186, A-242): a change is ordered after
// each other change of the difference that an edge of its entry names (the module version it is provisioned by, contributes
// to or is acted on by), with that edge's kind; its tier is one past the highest tier it is after. Changes on a cycle come in
// the last tier. The view orders nothing itself.
func order(d *meshwants.Difference, edges map[string][]meshwants.Edge, digest map[string]string) {
if len(d.Changes) == 0 {
return
}
in := map[string]int{}
for i, c := range d.Changes {
in[c.Node+"/"+c.Module] = i
}
for i, c := range d.Changes {
self := c.Node + "/" + c.Module
seen := map[string]bool{}
for _, e := range edges[self] {
k := e.Node + "/" + e.Module
if _, ok := in[k]; !ok || k == self || seen[e.Kind+k] {
continue
}
seen[e.Kind+k] = true
d.Changes[i].After = append(d.Changes[i].After, meshwants.ChangeAfterItem{Node: e.Node, Module: e.Module, Kind: e.Kind})
}
slices.SortFunc(d.Changes[i].After, func(a, b meshwants.ChangeAfterItem) int {
return strings.Compare(a.Kind+"\x00"+a.Node+"\x00"+a.Module, b.Kind+"\x00"+b.Node+"\x00"+b.Module)
})
}
tier := map[int]int{}
var level func(int, map[int]bool) int
level = func(i int, visiting map[int]bool) int {
if t, ok := tier[i]; ok {
return t
}
if visiting[i] {
return -1
}
visiting[i] = true
t := 0
for _, a := range d.Changes[i].After {
l := level(in[a.Node+"/"+a.Module], visiting)
if l < 0 {
return -1
}
t = max(t, l+1)
}
delete(visiting, i)
tier[i] = t
return t
}
top := 0
for i := range d.Changes {
top = max(top, level(i, map[int]bool{}))
}
for i := range d.Changes {
if _, ok := tier[i]; !ok {
tier[i] = top + 1
}
}
for i, c := range d.Changes {
for len(d.Tiers) <= tier[i] {
d.Tiers = append(d.Tiers, nil)
}
d.Tiers[tier[i]] = append(d.Tiers[tier[i]], meshwants.NodeEntryRef{Node: c.Node, Module: c.Module, Digest: digest[c.Node+"/"+c.Module]})
}
d.Tiers = slices.DeleteFunc(d.Tiers, func(t []meshwants.NodeEntryRef) bool { return len(t) == 0 })
}
func entriesOf(ctx context.Context, o Objects, rev meshwants.AssignmentRevision) (map[string]meshwants.AssignmentEntry, error) {
out := make(map[string]meshwants.AssignmentEntry, len(rev.Entries))
for _, ref := range rev.Entries {
@@ -517,8 +517,18 @@ type CatalogOf struct {
Source int64 `json:"source,omitempty"`
}
// ChangeAfterItem is a type of the contract.
type ChangeAfterItem struct {
// the kind of the edge that orders the change after that one: provisioning, contribution or acting
Kind string `json:"kind"`
Module string `json:"module"`
Node string `json:"node"`
}
// Change is one module on a node whose version differs between two revisions.
type Change struct {
// the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512)
After []ChangeAfterItem `json:"after,omitempty"`
// the edges that moved
Edges []Edge `json:"edges,omitempty"`
// the fingerprint before; absent when it is new there
@@ -955,6 +955,30 @@
"$ref": "#/defs/Edge"
},
"description": "the edges that moved"
},
"after": {
"type": "array",
"items": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"module": {
"type": "string"
},
"kind": {
"type": "string",
"description": "the kind of the edge that orders the change after that one: provisioning, contribution or acting"
}
},
"required": [
"node",
"module",
"kind"
]
},
"description": "the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512)"
}
},
"required": [
@@ -186,8 +186,17 @@ export interface CatalogOf {
"source"?: number;
}
export interface ChangeAfterItem {
/** the kind of the edge that orders the change after that one: provisioning, contribution or acting */
"kind": string;
"module": string;
"node": string;
}
/** one module on a node whose version differs between two revisions */
export interface Change {
/** the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512) */
"after"?: ChangeAfterItem[];
/** the edges that moved */
"edges"?: Edge[];
/** the fingerprint before; absent when it is new there */