Design 47 at hq main b229f5c4, lines 1353-1366 (each entry one module version on a node, its edges provisioning and acting; each revision naming its inputs, kept as objects in the old section with its cap, served by the seat's verbs), 1385-1388 (under 256 KiB for 120 modules on four nodes; a 512 MiB cap), 1416-1418 and 522-525 (a node's entries from the catalog graph of the branch its placement names, by default the registration's first), 909-914 (the placement's build is the version), 1835-1836 (an edge from the acting part's version to each version its rule names), and the Reverted table (assignment graph revisions as objects, form 1). internal/assigngraph computes a revision from the halves held per branch, the placements and the capability readings; keeps it numbered after the newest when it differs, each entry once by its digest; and answers revision, entry and difference from the kept objects only. The manifest serves the three verbs and declares the objects assignment-revisions. A placed module whose entry cannot be computed is named in Uncomputed, never left out in silence. Not built here (the design does not say, or held): contribution edges; provisioning to a provider no binding or pin names; tiers; a candidate's revision (W5.9); removal of a revision no record pins (pins are W5.9's). The process does not compute and keep yet: that needs the registrations' first view and a catalog half per branch (handover). Milestone 47/G, issue 1966. Tests: TestTheAssignmentGraphIsComputedFromItsInputsAndKept, TestARevisionOf120ModulesOnFourNodesFitsOneValue, TestTheSeatsVerbs/1953/D4-D6 (failed before), TestManifest's objects row (failed with the old manifest). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ Builds: design 47 G §173, §175, §176, §180, §194, §209, §223, §263, §346
157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
package assigngraph
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
|
)
|
|
|
|
// ErrNoCandidate answers a verb asked of a candidate's revision: the assigner keeps none (the candidate's graph and the
|
|
// pins from the gate are milestone 47/G's W5.9, held with research 086's fold).
|
|
var ErrNoCandidate = errors.New("the assigner keeps no candidate's assignment graph revision")
|
|
|
|
// ErrNoRevision answers a verb asked while no trunk revision is kept yet.
|
|
var ErrNoRevision = errors.New("no assignment graph revision is kept yet")
|
|
|
|
func revisionOf(ctx context.Context, o Objects, revision int64, group, heads string) (meshwants.AssignmentRevision, error) {
|
|
if group != "" || heads != "" {
|
|
return meshwants.AssignmentRevision{}, fmt.Errorf("%w (group %q, heads %q)", ErrNoCandidate, group, heads)
|
|
}
|
|
if revision > 0 {
|
|
return ReadRevision(ctx, o, revision)
|
|
}
|
|
rev, has, err := Newest(ctx, o)
|
|
if err == nil && !has {
|
|
err = ErrNoRevision
|
|
}
|
|
return rev, err
|
|
}
|
|
|
|
// Revision answers the seat's verb revision: one kept revision, the trunk's newest when none is named.
|
|
func Revision(ctx context.Context, o Objects, in meshwants.RevisionInput) (meshwants.RevisionAnswer, error) {
|
|
rev, err := revisionOf(ctx, o, in.Revision, in.Group, in.Heads)
|
|
return meshwants.RevisionAnswer{Revision: rev}, err
|
|
}
|
|
|
|
// Entry answers the seat's verb entry: one entry whole, by its digest, or by its node and module in a revision (the
|
|
// trunk's newest when none is named); an acting edge names the seat whose rule gave it.
|
|
func Entry(ctx context.Context, o Objects, in meshwants.EntryInput) (meshwants.EntryAnswer, error) {
|
|
digest := in.Digest
|
|
if digest == "" {
|
|
if in.Node == "" || in.Module == "" {
|
|
return meshwants.EntryAnswer{}, errors.New("entry takes a digest, or a node and a module")
|
|
}
|
|
rev, err := revisionOf(ctx, o, in.Revision, in.Group, in.Heads)
|
|
if err != nil {
|
|
return meshwants.EntryAnswer{}, err
|
|
}
|
|
for _, ref := range rev.Entries {
|
|
if ref.Node == in.Node && ref.Module == in.Module {
|
|
digest = ref.Digest
|
|
}
|
|
}
|
|
if digest == "" {
|
|
return meshwants.EntryAnswer{}, fmt.Errorf("revision %d has no entry of %s on %s", rev.Revision, in.Module, in.Node)
|
|
}
|
|
}
|
|
e, err := ReadEntry(ctx, o, digest)
|
|
return meshwants.EntryAnswer{Digest: digest, Entry: e}, err
|
|
}
|
|
|
|
// Difference answers the seat's verb difference: two kept revisions compared, each module on a node whose version
|
|
// differs, with the edges that moved, and the nodes it touches.
|
|
func Difference(ctx context.Context, o Objects, in meshwants.DifferenceInput) (meshwants.Difference, error) {
|
|
if in.Group != "" || in.Heads != "" {
|
|
return meshwants.Difference{}, fmt.Errorf("%w (group %q, heads %q)", ErrNoCandidate, in.Group, in.Heads)
|
|
}
|
|
if in.From <= 0 || in.To <= 0 {
|
|
return meshwants.Difference{}, errors.New("difference takes the two kept revisions it compares, from and to")
|
|
}
|
|
from, err := ReadRevision(ctx, o, in.From)
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
to, err := ReadRevision(ctx, o, in.To)
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
return Compare(ctx, o, from, to)
|
|
}
|
|
|
|
// Compare is the graph difference of two revisions.
|
|
func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevision) (meshwants.Difference, error) {
|
|
d := meshwants.Difference{From: from.Revision, To: to.Revision, Changes: []meshwants.Change{}}
|
|
was, err := entriesOf(ctx, o, from)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
now, err := entriesOf(ctx, o, to)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
nodes := map[string]bool{}
|
|
for _, k := range slices.Sorted(maps.Keys(union(was, now))) {
|
|
a, hadA := was[k]
|
|
b, hasB := now[k]
|
|
if hadA && hasB && a.Fp == b.Fp {
|
|
continue
|
|
}
|
|
c := meshwants.Change{}
|
|
if hadA {
|
|
c.Node, c.Module, c.From = a.Node, a.Module, a.Fp
|
|
}
|
|
if hasB {
|
|
c.Node, c.Module, c.To = b.Node, b.Module, b.Fp
|
|
}
|
|
c.Edges = moved(a.Edges, b.Edges)
|
|
d.Changes = append(d.Changes, c)
|
|
nodes[c.Node] = true
|
|
}
|
|
d.Nodes = slices.Sorted(maps.Keys(nodes))
|
|
return d, nil
|
|
}
|
|
|
|
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 {
|
|
e, err := ReadEntry(ctx, o, ref.Digest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("revision %d: %w", rev.Revision, err)
|
|
}
|
|
out[ref.Node+"/"+ref.Module] = e
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func union(a, b map[string]meshwants.AssignmentEntry) map[string]bool {
|
|
out := map[string]bool{}
|
|
for k := range a {
|
|
out[k] = true
|
|
}
|
|
for k := range b {
|
|
out[k] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// moved are the edges in one list and not the other.
|
|
func moved(a, b []meshwants.Edge) []meshwants.Edge {
|
|
var out []meshwants.Edge
|
|
for _, e := range b {
|
|
if !slices.Contains(a, e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
for _, e := range a {
|
|
if !slices.Contains(b, e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
sortEdges(out)
|
|
return out
|
|
}
|