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
179 lines
7.9 KiB
Go
179 lines
7.9 KiB
Go
package index
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// The catalog graph (novox/hq ADR 0656; design 47, *Two graphs, one source of truth*): at each revision one entry per
|
|
// module, as its manifest declares it, in the seat mesh-catalog's layout (its definition's CatalogEntry), and the
|
|
// revision a list of its entries' digests, each entry kept once as an object named by its digest
|
|
// (catalog-revisions: trunk.<revision>, candidate.<group>.<heads>, entry.<digest>).
|
|
|
|
// CatalogEntryLayout is the layout version an entry and a revision are written in.
|
|
const CatalogEntryLayout = 1
|
|
|
|
// manifestDigestPrefix is how an entry's field manifest-digest begins, as every digest a record names (design 47 G §578).
|
|
const manifestDigestPrefix = "sha256:"
|
|
|
|
// EntryManifestDigest is the entry's field manifest-digest for the module's digest bare, the 64 hex digits the module's
|
|
// value and the key of `manifests` hold: `sha256:` and the digits (design 47 G §578). A digest already prefixed stays, and
|
|
// none stays none.
|
|
func EntryManifestDigest(bare string) string {
|
|
if bare == "" || strings.HasPrefix(bare, manifestDigestPrefix) {
|
|
return bare
|
|
}
|
|
return manifestDigestPrefix + bare
|
|
}
|
|
|
|
// BareManifestDigest is the 64 hex digits of an entry's manifest-digest: the module's value and the key of `manifests`
|
|
// (design 47 G §578). An entry taken in before G carries the digits alone, which is what its module value held (§609), and
|
|
// stays as it is.
|
|
func BareManifestDigest(field string) string { return strings.TrimPrefix(field, manifestDigestPrefix) }
|
|
|
|
// CatalogEntryOf is the catalog graph entry of one module: the module graph's node of it as a recompute computed it
|
|
// (its build, provision and verb edges, the verb edges left out on a cycle, ADR 0614), with where the trunk holds it,
|
|
// its manifest's digest and verdict, the main package its process runs (MainOf), the package versions it builds against
|
|
// and what the indexer computes its next revision from (Inputs: the lead's ruling on milestone 47/G finding 1), so that
|
|
// ModuleValueOf gives the module's value back from the entry alone. The acting rules are the revision's to add.
|
|
func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, m catalogue.Manifest, raw string, n graph.Node) meshcatalog.CatalogEntry {
|
|
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), 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
|
|
}
|
|
in := meshcatalog.EntryInputs{Toolchains: maps.Clone(cur.Toolchains), Paths: maps.Clone(cur.Paths), Problems: cur.Problems,
|
|
Warnings: cur.Warnings, Unknown: cur.Unknown, Src: cur.Src, FpSrc: cur.FpSrc}
|
|
if cur.Inputs2 != nil {
|
|
from := meshcatalog.FingerprintInputs(*cur.Inputs2)
|
|
in.From = &from
|
|
}
|
|
if !reflect.ValueOf(in).IsZero() {
|
|
e.Inputs = &in
|
|
}
|
|
for language, t := range n.Toolchains {
|
|
if e.Toolchains == nil {
|
|
e.Toolchains = map[string]meshcatalog.BuildEdge{}
|
|
}
|
|
e.Toolchains[language] = meshcatalog.BuildEdge{Module: t.Module, Version: t.Version, Recipe: t.Recipe}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// ModuleValueOf is the module's value as the indexer computes with it, read back from its catalog graph entry: the
|
|
// inverse of CatalogEntryOf, by which the indexer's view of the trunk is the newest trunk revision's entries (the lead's
|
|
// ruling on milestone 47/G finding 1; ADR 0656 §6: one form of the fact). The old line's fields (the old-form
|
|
// fingerprint, the reader needs) are not kept.
|
|
func ModuleValueOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue {
|
|
v := meshcatalog.ModulesByModuleValue{V: keys.Version, Repository: e.Repository, Path: e.Path, Commit: e.Commit, Manifest: BareManifestDigest(e.ManifestDigest),
|
|
Verdict: e.Verdict, Fp2: e.Fp, None2: e.None, Packages: maps.Clone(e.Packages)}
|
|
if in := e.Inputs; in != nil {
|
|
v.Toolchains, v.Paths, v.Problems, v.Warnings, v.Unknown = maps.Clone(in.Toolchains), maps.Clone(in.Paths), in.Problems, in.Warnings, in.Unknown
|
|
v.Src, v.FpSrc = in.Src, in.FpSrc
|
|
if in.From != nil {
|
|
from := meshcatalog.ModulesByModuleValueInputs2(*in.From)
|
|
v.Inputs2 = &from
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// EncodeEntry is an entry's bytes and their sha256 in hex, the name of its object: the same entry in two revisions is
|
|
// one object (design 47, *An unchanged entry is stored once*).
|
|
func EncodeEntry(e meshcatalog.CatalogEntry) (string, []byte, error) {
|
|
data, err := json.Marshal(e)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("the entry of %s: %w", e.Module, err)
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:]), data, nil
|
|
}
|
|
|
|
// CatalogRevisionOf is a revision of entries, listed by module name with each entry's digest, and the entry objects it
|
|
// lists by digest, each once. The caller numbers the revision, names its kind (trunk or candidate) and what it was read
|
|
// from.
|
|
func CatalogRevisionOf(rev meshcatalog.CatalogRevision, entries map[string]meshcatalog.CatalogEntry) (meshcatalog.CatalogRevision, map[string][]byte, error) {
|
|
rev.V, rev.Entries = CatalogEntryLayout, make([]meshcatalog.EntryRef, 0, len(entries))
|
|
objects := map[string][]byte{}
|
|
for _, module := range slices.Sorted(maps.Keys(entries)) {
|
|
digest, data, err := EncodeEntry(entries[module])
|
|
if err != nil {
|
|
return meshcatalog.CatalogRevision{}, nil, err
|
|
}
|
|
objects[digest] = data
|
|
rev.Entries = append(rev.Entries, meshcatalog.EntryRef{Module: module, Digest: digest})
|
|
}
|
|
return rev, objects, nil
|
|
}
|
|
|
|
// MainOf is the Go main package the check's first tier builds when the module acts (the seat mesh-catalog's entry
|
|
// field main): the from of the first Go artifact a process resource of the manifest runs, as the manifest names it;
|
|
// empty when it runs none.
|
|
func MainOf(m catalogue.Manifest) string {
|
|
if m.Build == nil {
|
|
return ""
|
|
}
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) != "process" {
|
|
continue
|
|
}
|
|
name := fmt.Sprint(r["artifact"])
|
|
for _, a := range m.Build.Artifacts {
|
|
if a.Name == name && a.Language == "go" && a.From != "" {
|
|
return a.From
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// manifestWhole is the validated manifest the entry carries whole (design 47 G §443): the bytes the forge answered,
|
|
// read as the object they are; an entry of a module whose manifest is not kept carries an empty object.
|
|
func manifestWhole(raw string) map[string]any {
|
|
whole := map[string]any{}
|
|
if raw == "" {
|
|
return whole
|
|
}
|
|
if err := json.Unmarshal([]byte(raw), &whole); err != nil {
|
|
return 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
|
|
}
|