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) +}