From 642a0ad2f951e8eded9af8996239b8683ce29ce0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:19:53 +0100 Subject: [PATCH] AS14 (b, c): a requirement points first to the named seat's holder, and a contributed one carries a contribution edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../cmd/mesh-delivery-assigner/graphkept.go | 5 + .../cmd/mesh-delivery-assigner/halves.go | 2 + .../mesh-delivery-assigner/pipelines_test.go | 8 +- .../internal/assigngraph/compute.go | 124 +++++++++++++----- .../internal/assigngraph/graph_test.go | 79 +++++++---- .../go/seats/meshcatalog/contract.go | 2 + .../go/seats/meshwants/contract.go | 21 ++- .../seats/mesh-catalog/seat.json | 7 + .../seats/mesh-wants/seat.json | 37 ++++-- .../ts/seats/meshcatalog/index.d.ts | 2 + .../ts/seats/meshwants/index.d.ts | 21 ++- .../internal/index/catalog_graph.go | 25 +++- .../internal/index/catalog_graph_test.go | 24 ++++ modules/mesh-delivery-sdk/graph/entry.go | 18 ++- 14 files changed, 294 insertions(+), 81 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go index 77980e47..a6bccf5b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go @@ -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 { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go index 8d617071..8aa16801 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go @@ -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} } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pipelines_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pipelines_test.go index 19a2d4a4..39429d02 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pipelines_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pipelines_test.go @@ -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) diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go index 2b5b8750..4372c3dc 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go @@ -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 +} diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go index 2827b7f6..21099c45 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go @@ -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) } }) } diff --git a/modules/mesh-delivery-contracts/go/seats/meshcatalog/contract.go b/modules/mesh-delivery-contracts/go/seats/meshcatalog/contract.go index 504752fc..a0a56a2a 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshcatalog/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshcatalog/contract.go @@ -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 diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go index e73a7cb0..c3ab5467 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go @@ -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. diff --git a/modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json b/modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json index 0233171e..c9c69cad 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json @@ -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": { diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json index bdb5915b..8100f320 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json @@ -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": { diff --git a/modules/mesh-delivery-contracts/ts/seats/meshcatalog/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshcatalog/index.d.ts index dfe29ba2..8c717ae2 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshcatalog/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshcatalog/index.d.ts @@ -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 */ 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 ef4554b2..57ec1e0f 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts @@ -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 { diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_graph.go b/modules/mesh-delivery-indexer/internal/index/catalog_graph.go index 82fe97ef..24c653b1 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_graph.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_graph.go @@ -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 +} diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_graph_test.go b/modules/mesh-delivery-indexer/internal/index/catalog_graph_test.go index d1066aee..e2502b19 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_graph_test.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_graph_test.go @@ -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) + } +} diff --git a/modules/mesh-delivery-sdk/graph/entry.go b/modules/mesh-delivery-sdk/graph/entry.go index 49341939..a2fa9ab5 100644 --- a/modules/mesh-delivery-sdk/graph/entry.go +++ b/modules/mesh-delivery-sdk/graph/entry.go @@ -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) +}