From c8d842b9247962da3fb99cbeacb6814944710da0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:28:02 +0100 Subject: [PATCH] VW2: the assigner's difference fills Tiers and each change's after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../internal/assigngraph/graph_test.go | 53 ++++++++++++ .../internal/assigngraph/verbs.go | 86 +++++++++++++++++++ .../go/seats/meshwants/contract.go | 10 +++ .../seats/mesh-wants/seat.json | 24 ++++++ .../ts/seats/meshwants/index.d.ts | 9 ++ 5 files changed, 182 insertions(+) diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go index 767c88e6..4cead6f5 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go @@ -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) + } +} diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/verbs.go b/modules/mesh-delivery-assigner/internal/assigngraph/verbs.go index 3b7d2d6c..4fccbefb 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/verbs.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/verbs.go @@ -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 { diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go index 23b263fc..7b2db68b 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go @@ -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 diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json index e0564718..1b7e0b87 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json @@ -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": [ diff --git a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts index ee5a58f3..8807aece 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts @@ -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 */