AS14 (b, c): a requirement points first to the named seat's holder, and a contributed one carries a contribution edge

The seat whose definition delivers a requirement, among those the entry's
catalog graph revision names, is resolved first: its holder from the
placements' holds, as seat-holders keeps them; a delivering seat no
placement holds holds the entry, naming the seat. A hold now names the
requirement, the seat and each candidate {node, module}; an optional
requirement nothing provides is left absent. The catalog entry gains
contributes, filled by the indexer, and requires names every requirement
as Wants lists them, optional ones included (the module graph and the
assigner's halves leave the optional ones out by the entry's manifest).
A contributed requirement gives a contribution edge to the same provider.

Builds: design 47 G §708, §709, §357; lead answers A-167, A-229

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:19:53 +01:00
parent 12e1fd4403
commit 642a0ad2f9
14 changed files with 294 additions and 81 deletions
@@ -51,6 +51,11 @@ func (p *passer) keepGraph(ctx context.Context) {
return
}
in := assigngraph.Inputs{Halves: g.halves(), Placements: p.w.assignments(), Capabilities: p.capabilityDigests()}
// The seat definitions read through the seat mesh-catalog's verb seat-definitions say which seat delivers a requirement
// (design 47 G §708).
if r := p.seats.Reader(); r != nil {
in.Definitions = r.Definitions()
}
// With no pipeline ends kept, the wants are the trunk-only form (pass.go): a placement naming no branch runs the trunk's.
in.FirstBranch = func(string) string { return trunkBranch }
if p.ends != nil {
@@ -108,6 +108,8 @@ func fromEntry(e meshcatalog.CatalogEntry) (meshcatalog.ModulesByModuleValue, gr
return v, n, &parsed{module: e.Module, err: "it does not decode: " + err.Error()}
}
n.Serves = graph.ServesOf(man)
// The entry's requires name the optional ones too; the node means those required, not optionally (A-229).
n.Requires = slices.DeleteFunc(slices.Clone(n.Requires), man.IsOptional)
return v, n, &parsed{module: e.Module, manifest: man}
}
@@ -293,8 +293,9 @@ func TestTheCatalogHalfIsTakenInPerBranch(t *testing.T) {
woken := 0
h := &halves{w: w, wake: func() { woken++ }, logf: t.Logf}
entry := func(fp string) json.RawMessage {
raw, _ := json.Marshal(meshcatalog.CatalogEntry{Module: "web", Fp: fp, Repository: "novox/web", Commit: "c1", Manifest: map[string]any{"module": "web", "version": "1"},
Invokes: []string{"seat:x.v"}})
raw, _ := json.Marshal(meshcatalog.CatalogEntry{Module: "web", Fp: fp, Repository: "novox/web", Commit: "c1",
Manifest: map[string]any{"module": "web", "version": "1", "requires": []any{"database", map[string]any{"name": "cache", "optional": true}}},
Invokes: []string{"seat:x.v"}, Requires: []string{"cache", "database"}})
return raw
}
h.changed("main", "web", entry("src2:1"), false)
@@ -306,7 +307,8 @@ func TestTheCatalogHalfIsTakenInPerBranch(t *testing.T) {
if got := w.manifest(v.Manifest); got.err != "" || got.manifest.Module != "web" {
t.Errorf("the entry's manifest is not read by its digest: %+v", got)
}
if n := w.graphOf("main")["web"]; n.FP != "src2:1" || !slices.Equal(n.Invokes, []string{"seat:x.v"}) {
// The entry's requires name the optional cache too; the graph node means those required, not optionally (A-229).
if n := w.graphOf("main")["web"]; n.FP != "src2:1" || !slices.Equal(n.Invokes, []string{"seat:x.v"}) || !slices.Equal(n.Requires, []string{"database"}) {
t.Errorf("graph node %+v", n)
}
h.changed("beta", "web", nil, true)
@@ -17,6 +17,7 @@ import (
"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/meshwants"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"mesh-delivery-assigner/internal/assignment"
)
@@ -24,6 +25,7 @@ import (
// The kinds of an edge of the assignment graph (ADR 0656; the seat's Edge).
const (
EdgeProvisioning = "provisioning"
EdgeContribution = "contribution"
EdgeActing = "acting"
)
@@ -38,6 +40,9 @@ type Inputs struct {
FirstBranch func(module string) string
// Capabilities is each node's capability reading, by its digest.
Capabilities []meshwants.AssignmentRevisionCapabilitiesItem
// Definitions are the seat definitions read whole through the seat mesh-catalog's verb seat-definitions, by seat: of
// them, those an entry's catalog graph revision names in its definitions say which seat delivers a requirement (§708).
Definitions map[string]seatdef.Definition
}
// Computed is one assignment graph revision as computed, before it is numbered and kept.
@@ -60,8 +65,12 @@ type placedAt struct {
entry meshwants.AssignmentEntry
langs map[string]bool
acts []actingRule
// provides is what its catalog entry provides; open its requirements its placement neither binds nor its node pins.
provides, open []string
// provides is what its catalog entry provides; contributes the requirements it carries contributed values to (§709).
provides, contributes []string
// delivering is, by requirement, the seat that delivers it among the definitions its revision names (§708).
delivering map[string]seatdef.Definition
// optional says a requirement is one it runs without (ADR 0330 §3), by the entry's manifest (A-229).
optional func(string) bool
}
type actingRule struct{ seat, step, on, bound string }
@@ -120,49 +129,42 @@ func Compute(in Inputs) Computed {
for _, r := range ce.Acts {
p.acts = append(p.acts, actingRule{seat: r.Seat, step: r.Step, on: r.On, bound: r.Bound})
}
p.provides = ce.Provides
for _, r := range ce.Requires {
prov, ok := m.Bindings[r]
if !ok {
prov, ok = in.Placements[node].Pins[r]
}
if ok {
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r})
} else {
p.open = append(p.open, r)
p.provides, p.contributes = ce.Provides, ce.Contributes
p.delivering = map[string]seatdef.Definition{}
for _, d := range half.Revision.Definitions {
if def, ok := in.Definitions[d.Seat]; ok && def.Delivers != "" {
if _, had := p.delivering[def.Delivers]; !had {
p.delivering[def.Delivers] = def
}
}
}
p.optional = func(string) bool { return false }
if man, err := manifestOf(ce); err == nil {
p.optional = man.IsOptional
}
placed = append(placed, p)
branches[branch] = true
}
}
// A requirement neither bound nor pinned points, by design 27's order (design 47 G §357), to the provider on the
// consumer's node, else to the only provider; otherwise the entry is held, naming the candidates, and nothing is guessed.
// Each requirement points, by design 27's order (design 47 G §357, §708), to the named seat's holder, else a pin, else
// the provider on the consumer's node, else the only provider; otherwise the entry is held, naming the candidates, and
// nothing is guessed. A requirement contributes names gets a contribution edge to the same provider (§709).
holders := assignment.SeatHoldersOf(in.Placements)
for _, p := range placed {
for _, r := range p.open {
var here, all []*placedAt
for _, q := range placed {
if q != p && slices.Contains(q.provides, r) {
all = append(all, q)
if q.node == p.node {
here = append(here, q)
}
}
}
ce := in.Halves[p.branch].Entries[p.placed.Module]
for _, r := range ce.Requires {
prov, held := resolve(p, r, placed, holders, in.Placements[p.node].Pins)
switch {
case len(here) == 1:
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: here[0].node, Module: here[0].placed.Module, Provision: r})
case len(here) == 0 && len(all) == 1:
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: all[0].node, Module: all[0].placed.Module, Provision: r})
default:
h := meshwants.Held{Provision: r}
for _, q := range all {
h.Candidates = append(h.Candidates, meshwants.Edge{Kind: EdgeProvisioning, Node: q.node, Module: q.placed.Module, Fp: q.entry.Fp, Provision: r})
case held != nil:
p.entry.Held = append(p.entry.Held, *held)
case prov.Module != "":
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r})
if slices.Contains(p.contributes, r) {
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeContribution, Node: prov.Node, Module: prov.Module, Provision: r})
}
sortEdges(h.Candidates)
p.entry.Held = append(p.entry.Held, h)
}
}
slices.SortFunc(p.entry.Held, func(a, b meshwants.Held) int { return strings.Compare(a.Requirement, b.Requirement) })
}
at := map[string]*placedAt{}
for _, p := range placed {
@@ -241,3 +243,57 @@ func Same(a, b meshwants.AssignmentRevision) bool {
return slices.Equal(a.Entries, b.Entries) && slices.Equal(a.Catalogs, b.Catalogs) && a.Placements == b.Placements &&
slices.Equal(a.Capabilities, b.Capabilities)
}
// resolve is the provider requirement r of p points to by §708's order, or the hold that says why none is: none and no
// hold for an optional requirement nothing provides (ADR 0330 §3).
func resolve(p *placedAt, r string, placed []*placedAt, holders map[string]mma.SeatHolders, pins map[string]mma.Provider) (mma.Provider, *meshwants.Held) {
// 1. The named seat's holder, from the placements' holds as the state seat-holders keeps them; a delivering seat no
// placement holds holds the entry, and no other step is tried.
if def, ok := p.delivering[r]; ok {
node := ""
if def.Scope == seatdef.ScopeNode {
node = p.node
}
key := def.Seat
if node != "" {
key += "." + node
}
if sh := holders[key]; len(sh.Holders) > 0 {
return mma.Provider{Node: sh.Holders[0].Node, Module: sh.Holders[0].Module}, nil
}
return mma.Provider{}, &meshwants.Held{Requirement: r, Seat: def.Seat, Candidates: []meshwants.HeldCandidate{}}
}
// 2. A pin: the placed module's bindings, else its node's pins.
if prov, ok := p.placed.Bindings[r]; ok {
return prov, nil
}
if prov, ok := pins[r]; ok {
return prov, nil
}
// 3. The provider on the node; 4. the only provider.
var here, all []*placedAt
for _, q := range placed {
if q != p && slices.Contains(q.provides, r) {
all = append(all, q)
if q.node == p.node {
here = append(here, q)
}
}
}
switch {
case len(here) == 1:
return mma.Provider{Node: here[0].node, Module: here[0].placed.Module}, nil
case len(here) == 0 && len(all) == 1:
return mma.Provider{Node: all[0].node, Module: all[0].placed.Module}, nil
case len(all) == 0 && p.optional(r):
return mma.Provider{}, nil
}
h := &meshwants.Held{Requirement: r, Candidates: []meshwants.HeldCandidate{}}
for _, q := range all {
h.Candidates = append(h.Candidates, meshwants.HeldCandidate{Node: q.node, Module: q.placed.Module})
}
slices.SortFunc(h.Candidates, func(a, b meshwants.HeldCandidate) int {
return strings.Compare(a.Node+"/"+a.Module, b.Node+"/"+b.Module)
})
return mma.Provider{}, h
}
@@ -13,6 +13,7 @@ import (
"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"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
type memObjects map[string][]byte
@@ -98,7 +99,7 @@ func TestTheAssignmentGraphIsComputedFromItsInputsAndKept(t *testing.T) {
t.Errorf("web on laptop's edges: %+v", e)
}
// Its secrets no placed module provides: held on both nodes, naming no candidate.
if h := webA.Held; len(h) != 1 || h[0].Provision != "secrets" || len(h[0].Candidates) != 0 {
if h := webA.Held; len(h) != 1 || h[0].Requirement != "secrets" || len(h[0].Candidates) != 0 {
t.Errorf("web on anchor is held on %+v", h)
}
if e := byNode["anchor/postgres"].Edges; len(e) != 1 || e[0].Module != "node-engine" {
@@ -154,26 +155,48 @@ func TestTheAssignmentGraphIsComputedFromItsInputsAndKept(t *testing.T) {
}
}
// Design 47 G §357 and A-167: a requirement neither bound nor pinned points to the provider on the consumer's node, else
// to the only provider; otherwise the entry is held, naming the candidates, and nothing is guessed.
// Design 47 G §357, §708, §709 and A-167, A-229: a requirement points to the named seat's holder, else a pin, else the
// provider on the consumer's node, else the only provider; otherwise the entry is held, naming the candidates (and the
// unheld seat), and nothing is guessed. An optional requirement nothing provides is left absent; a requirement the
// manifest's contributes names gets a contribution edge to the same provider.
func TestARequirementPointsByTheOrderOrIsHeld(t *testing.T) {
h := half("main", 3,
meshcatalog.CatalogEntry{Module: "kv", Fp: "src2:k1", Provides: []string{"cache"}},
meshcatalog.CatalogEntry{Module: "web", Fp: "src2:w1", Requires: []string{"cache"}})
for _, c := range []struct {
name string
kv []string // the nodes kv is placed on
pin bool // web's node pins kv on laptop
edge string // the node of the provisioning edge, "" for none
held string // the candidates' nodes of the hold, "-" for no hold
name string
kv []string // the nodes kv is placed on
pin bool // web's node pins kv on laptop
seat string // the seat delivering cache: "" none, "held" held by kv on server, "unheld" held by none
optional bool // web requires cache optionally
contributes bool // web's manifest contributes to cache
edge string // the node of the provisioning edge, "" for none
held string // the candidates' nodes of the hold, "-" for no hold
heldSeat string // the seat the hold names
}{
{name: "the provider on the consumer's node", kv: []string{"anchor", "laptop"}, edge: "anchor", held: "-"},
{name: "the only provider", kv: []string{"laptop"}, edge: "laptop", held: "-"},
{name: "a pin before the node's own", kv: []string{"anchor", "laptop"}, pin: true, edge: "laptop", held: "-"},
{name: "two providers elsewhere: held", kv: []string{"laptop", "server"}, held: "laptop,server"},
{name: "none: held naming none", held: ""},
{name: "the named seat's holder before the provider on the node", kv: []string{"anchor", "server"}, seat: "held", edge: "server", held: "-"},
{name: "a delivering seat no placement holds: held naming it", kv: []string{"anchor"}, seat: "unheld", held: "", heldSeat: "cache-seat"},
{name: "an optional requirement nothing provides: absent", optional: true, held: "-"},
{name: "an optional requirement with two providers elsewhere: held", optional: true, kv: []string{"laptop", "server"}, held: "laptop,server"},
{name: "a contributed requirement: a contribution edge too", kv: []string{"laptop"}, contributes: true, edge: "laptop", held: "-"},
} {
t.Run(c.name, func(t *testing.T) {
web := meshcatalog.CatalogEntry{Module: "web", Fp: "src2:w1", Requires: []string{"cache"},
Manifest: map[string]any{"module": "web", "version": "1", "requires": []any{"cache"}}}
if c.optional {
web.Manifest["requires"] = []any{map[string]any{"name": "cache", "optional": true}}
}
if c.contributes {
web.Contributes = []string{"cache"}
}
h := half("main", 3, meshcatalog.CatalogEntry{Module: "kv", Fp: "src2:k1", Provides: []string{"cache"}}, web)
defs := map[string]seatdef.Definition{}
if c.seat != "" {
h.Revision.Definitions = []meshcatalog.CatalogRevisionDefinitionsItem{{Seat: "cache-seat", Digest: "sha256:d"}}
defs["cache-seat"] = seatdef.Definition{Seat: "cache-seat", Scope: seatdef.ScopeMesh, Delivers: "cache"}
}
pl := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}}
if c.pin {
pl["anchor"] = mma.Assignments{Node: "anchor", Modules: pl["anchor"].Modules, Pins: map[string]mma.Provider{"cache": {Node: "laptop", Module: "kv"}}}
@@ -183,29 +206,41 @@ func TestARequirementPointsByTheOrderOrIsHeld(t *testing.T) {
a.Node, a.Modules = n, append(a.Modules, mma.PlacedModule{Module: "kv"})
pl[n] = a
}
out := Compute(Inputs{Halves: map[string]meshcatalogclient.Half{"main": h}, Placements: pl, FirstBranch: func(string) string { return "main" }})
var web meshwants.AssignmentEntry
if c.seat == "held" {
a := pl["server"]
a.Holds = append(a.Holds, mma.Holding{Claim: "cache-seat", Module: "kv"})
pl["server"] = a
}
out := Compute(Inputs{Halves: map[string]meshcatalogclient.Half{"main": h}, Placements: pl, FirstBranch: func(string) string { return "main" },
Definitions: defs})
var got meshwants.AssignmentEntry
for _, ref := range out.Revision.Entries {
if ref.Node == "anchor" && ref.Module == "web" {
web = out.Entries[ref.Digest]
got = out.Entries[ref.Digest]
}
}
edge := ""
for _, e := range web.Edges {
edge, contribution := "", ""
for _, e := range got.Edges {
if e.Kind == EdgeProvisioning && e.Provision == "cache" {
edge = e.Node
}
if e.Kind == EdgeContribution && e.Provision == "cache" {
contribution = e.Node + "/" + e.Module
}
}
held := "-"
if len(web.Held) == 1 && web.Held[0].Provision == "cache" {
held, heldSeat := "-", ""
if len(got.Held) == 1 && got.Held[0].Requirement == "cache" {
var nodes []string
for _, cand := range web.Held[0].Candidates {
for _, cand := range got.Held[0].Candidates {
nodes = append(nodes, cand.Node)
}
held = strings.Join(nodes, ",")
held, heldSeat = strings.Join(nodes, ","), got.Held[0].Seat
}
if edge != c.edge || held != c.held || len(web.Held) > 1 {
t.Errorf("web points to %q, held on %q (%+v); want %q, %q", edge, held, web.Held, c.edge, c.held)
if edge != c.edge || held != c.held || heldSeat != c.heldSeat || len(got.Held) > 1 {
t.Errorf("web points to %q, held on %q seat %q (%+v); want %q, %q, %q", edge, held, heldSeat, got.Held, c.edge, c.held, c.heldSeat)
}
if want := map[bool]string{true: c.edge + "/kv"}[c.contributes]; contribution != want {
t.Errorf("web's contribution edge points to %q, want %q", contribution, want)
}
})
}
@@ -732,6 +732,8 @@ type CatalogEntry struct {
Bases []string `json:"bases,omitempty"`
// the merge commit it was last changed at
Commit string `json:"commit"`
// the requirements the manifest's contributes object names as keys, sorted, its local names not carried; each is also in requires (design 47 G section 709)
Contributes []string `json:"contributes,omitempty"`
// the edges into this module's artifacts, each from the artifact that comes first; the build and the delivery order by these alone
Edges []Edge `json:"edges,omitempty"`
// its fingerprint (src2: and 64 hex digits); absent when it has none
@@ -564,9 +564,9 @@ type Edge struct {
Kind string `json:"kind"`
Module string `json:"module"`
Node string `json:"node"`
// for provisioning, the provision
// for provisioning and contribution, the requirement
Provision string `json:"provision,omitempty"`
// for contribution, the seat it contributes to; for acting, the seat whose rule gave the edge
// for acting only, the seat whose rule gave the edge
Seat string `json:"seat,omitempty"`
// for acting, the step: take-in, validate, check, build, compose, grant or place
Step string `json:"step,omitempty"`
@@ -584,11 +584,20 @@ type Fill struct {
Store string `json:"store"`
}
// Held is one requirement an entry is held on, for want of a provider.
// Held is one requirement an entry is held on, for want of a provider (design 47 G section 708): the assigner writes no want from a held entry, so the node keeps what it runs.
type Held struct {
// a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it
Candidates []Edge `json:"candidates,omitempty"`
Provision string `json:"provision"`
// each placed module that provides it, sorted; empty when none does
Candidates []HeldCandidate `json:"candidates,omitempty"`
// the requirement no step of the order resolves
Requirement string `json:"requirement"`
// the seat that delivers the requirement and that no placement holds; only where the named seat's step held it
Seat string `json:"seat,omitempty"`
}
// HeldCandidate is one placed provider of a held requirement.
type HeldCandidate struct {
Module string `json:"module"`
Node string `json:"node"`
}
// NodeEntryRef is a type of the contract.
@@ -1932,6 +1932,13 @@
"type": "string"
}
},
"contributes": {
"type": "array",
"items": {
"type": "string"
},
"description": "the requirements the manifest's contributes object names as keys, sorted, its local names not carried; each is also in requires (design 47 G section 709)"
},
"awaits": {
"type": "array",
"items": {
@@ -773,11 +773,11 @@
},
"provision": {
"type": "string",
"description": "for provisioning, the provision"
"description": "for provisioning and contribution, the requirement"
},
"seat": {
"type": "string",
"description": "for contribution, the seat it contributes to; for acting, the seat whose rule gave the edge"
"description": "for acting only, the seat whose rule gave the edge"
},
"step": {
"type": "string",
@@ -844,21 +844,42 @@
},
"Held": {
"type": "object",
"description": "one requirement an entry is held on, for want of a provider",
"description": "one requirement an entry is held on, for want of a provider (design 47 G section 708): the assigner writes no want from a held entry, so the node keeps what it runs",
"properties": {
"provision": {
"type": "string"
"requirement": {
"type": "string",
"description": "the requirement no step of the order resolves"
},
"seat": {
"type": "string",
"description": "the seat that delivers the requirement and that no placement holds; only where the named seat's step held it"
},
"candidates": {
"type": "array",
"description": "a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it",
"description": "each placed module that provides it, sorted; empty when none does",
"items": {
"$ref": "#/defs/Edge"
"$ref": "#/defs/HeldCandidate"
}
}
},
"required": [
"provision"
"requirement"
]
},
"HeldCandidate": {
"type": "object",
"description": "one placed provider of a held requirement",
"properties": {
"node": {
"type": "string"
},
"module": {
"type": "string"
}
},
"required": [
"node",
"module"
]
},
"NodeEntryRef": {
@@ -243,6 +243,8 @@ export interface CatalogEntry {
"bases"?: string[];
/** the merge commit it was last changed at */
"commit": string;
/** the requirements the manifest's contributes object names as keys, sorted, its local names not carried; each is also in requires (design 47 G section 709) */
"contributes"?: string[];
/** the edges into this module's artifacts, each from the artifact that comes first; the build and the delivery order by these alone */
"edges"?: Edge[];
/** its fingerprint (src2: and 64 hex digits); absent when it has none */
@@ -232,9 +232,9 @@ export interface Edge {
"kind": string;
"module": string;
"node": string;
/** for provisioning, the provision */
/** for provisioning and contribution, the requirement */
"provision"?: string;
/** for contribution, the seat it contributes to; for acting, the seat whose rule gave the edge */
/** for acting only, the seat whose rule gave the edge */
"seat"?: string;
/** for acting, the step: take-in, validate, check, build, compose, grant or place */
"step"?: string;
@@ -252,11 +252,20 @@ export interface Fill {
"store": string;
}
/** one requirement an entry is held on, for want of a provider */
/** one requirement an entry is held on, for want of a provider (design 47 G section 708): the assigner writes no want from a held entry, so the node keeps what it runs */
export interface Held {
/** a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it */
"candidates"?: Edge[];
"provision": string;
/** each placed module that provides it, sorted; empty when none does */
"candidates"?: HeldCandidate[];
/** the requirement no step of the order resolves */
"requirement": string;
/** the seat that delivers the requirement and that no placement holds; only where the named seat's step held it */
"seat"?: string;
}
/** one placed provider of a held requirement */
export interface HeldCandidate {
"module": string;
"node": string;
}
export interface NodeEntryRef {
@@ -51,7 +51,14 @@ func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, m catal
e := meshcatalog.CatalogEntry{V: CatalogEntryLayout, Module: module, Repository: cur.Repository, Path: cur.Path,
Commit: cur.Commit, Manifest: manifestWhole(raw), ManifestDigest: EntryManifestDigest(cur.Manifest), Verdict: cur.Verdict, Fp: n.FP, None: n.None, Bases: n.Bases,
Packages: maps.Clone(cur.Packages), Provides: n.Provides, Requires: n.Requires, Awaits: n.Awaits, Invokes: n.Invokes,
Serves: n.Serves, LeftOut: n.LeftOut, Main: MainOf(m)}
Serves: n.Serves, LeftOut: n.LeftOut, Main: MainOf(m), Contributes: contributesOf(m)}
// requires names every requirement as catalogue.Manifest.Wants lists them, optional ones and those only contributes names
// included, since contributing to a requirement is requiring it (design 47 G §709, A-229).
for _, r := range m.Wants() {
if !slices.Contains(e.Requires, r) {
e.Requires = append(slices.Clone(e.Requires), r)
}
}
if n.FP == "" && n.None == "" {
// A module the recompute computed no node for (its manifest cannot be read) keeps what its value holds.
e.Fp, e.None = cur.Fp2, cur.None2
@@ -153,3 +160,19 @@ func manifestWhole(raw string) map[string]any {
}
return whole
}
// contributesOf is the entry's field contributes (design 47 G §709): the requirements the manifest's contributes object names
// as keys, sorted, its local names not carried; none when it names none.
func contributesOf(m catalogue.Manifest) []string {
var out []string
for r := range m.Contributes {
out = append(out, r)
}
for r := range m.ContributesMany {
if !slices.Contains(out, r) {
out = append(out, r)
}
}
slices.Sort(out)
return out
}
@@ -86,3 +86,27 @@ func TestAnImageArtifactsPackageNameIsItsModulesAndItsOwn(t *testing.T) {
t.Errorf("two modules' image artifacts of one name carry %v, want two package names", names)
}
}
// Design 47 G §709 and A-229: a manifest whose contributes names route gives an entry whose contributes and requires both
// name it; requires names an optional requirement too, as catalogue.Manifest.Wants lists them, beside the node's own.
func TestAnEntryNamesWhatItContributesToAndEveryRequirement(t *testing.T) {
raw := `{"module":"web","version":"1","requires":["database",{"name":"cache","optional":true}],"contributes":{"route":{"host":"web"}}}`
m, err := catalogue.Decode([]byte(raw))
if err != nil {
t.Fatal(err)
}
e := CatalogEntryOf("web", meshcatalog.ModulesByModuleValue{}, m, raw, graph.Node{Requires: []string{"database", graph.BusState}})
if !reflect.DeepEqual(e.Contributes, []string{"route"}) {
t.Errorf("contributes = %v, want [route]", e.Contributes)
}
if want := []string{"database", graph.BusState, "cache", "route"}; !reflect.DeepEqual(e.Requires, want) {
t.Errorf("requires = %v, want %v", e.Requires, want)
}
// Read back as the module graph's node, the optional requirement is told apart by the entry's manifest (A-229).
if n := graph.NodeOf(e); !reflect.DeepEqual(n.Requires, []string{"database", graph.BusState, "route"}) {
t.Errorf("the node's requires = %v, want the optional cache left out", n.Requires)
}
if e := CatalogEntryOf("kv", meshcatalog.ModulesByModuleValue{}, catalogue.Manifest{}, "", graph.Node{}); e.Contributes != nil {
t.Errorf("an entry of a manifest contributing nothing carries contributes %v", e.Contributes)
}
}
+17 -1
View File
@@ -1,10 +1,12 @@
package graph
import (
"encoding/json"
"maps"
"slices"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// NodeOf is the node of the module graph a catalog graph entry carries, as the indexer took it in (design 47, *Two
@@ -12,7 +14,7 @@ import (
// catalog half reads the module graph from its entries, never from the holder's state.
func NodeOf(e meshcatalog.CatalogEntry) Node {
n := Node{V: Version, FP: e.Fp, None: e.None, Repository: e.Repository, Path: e.Path, Bases: e.Bases,
Provides: e.Provides, Requires: e.Requires, Awaits: e.Awaits, Invokes: e.Invokes, Serves: e.Serves, LeftOut: e.LeftOut}
Provides: e.Provides, Requires: required(e), Awaits: e.Awaits, Invokes: e.Invokes, Serves: e.Serves, LeftOut: e.LeftOut}
if len(e.Packages) > 0 {
n.Packages = slices.Sorted(maps.Keys(e.Packages))
}
@@ -24,3 +26,17 @@ func NodeOf(e meshcatalog.CatalogEntry) Node {
}
return n
}
// required are the entry's requires a node means, those it requires not optionally: an entry's requires name the optional
// ones too, told apart by the entry's manifest (design 47 G §709, A-229).
func required(e meshcatalog.CatalogEntry) []string {
if len(e.Manifest) == 0 || len(e.Requires) == 0 {
return e.Requires
}
var m catalogue.Manifest
raw, err := json.Marshal(e.Manifest)
if err != nil || json.Unmarshal(raw, &m) != nil {
return e.Requires
}
return slices.DeleteFunc(slices.Clone(e.Requires), m.IsOptional)
}