The seat mesh-catalog's four verbs answered from the indexer's modules, manifests and graph, with the old catalog's field names (captured answers as fixtures); served once the indexer claims the seat, in the release after L3's (ADR 0604 §1, §2)

This commit is contained in:
2026-11-03 16:45:06 +01:00
parent 4f6e1021c6
commit 9f49bc92ad
8 changed files with 457 additions and 0 deletions
@@ -0,0 +1,101 @@
package main
import (
"fmt"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
// CatalogTools are the seat mesh-catalog's four verbs as tools named <seat>.<verb>, with the descriptions and inputs the
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
func CatalogTools(c index.CatalogRead) []stdio.Tool {
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
if !ok {
return nil
}
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
runs := map[string]func(map[string]any) (any, error){
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
"module": func(a map[string]any) (any, error) { return c.CatalogModuleAnswer(str(a, "module")) },
"provides": func(a map[string]any) (any, error) { return c.CatalogProvidesAnswer(str(a, "provision")) },
"dependents": func(a map[string]any) (any, error) { return c.CatalogDependentsAnswer(str(a, "module")) },
}
var out []stdio.Tool
for _, v := range seat.Serves {
if run, ok := runs[v.Name]; ok {
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
}
}
return out
}
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead {
return index.CatalogRead{
Modules: func() (map[string]delivery.Module, error) {
keys, err := modules.Keys()
if err != nil {
return nil, err
}
out := map[string]delivery.Module{}
for _, k := range keys {
e, err := modules.Get(k)
if err != nil {
return nil, fmt.Errorf("modules.%s: %w", k, err)
}
if e == nil {
continue
}
if m, err := delivery.DecodeModule(e.Value); err == nil {
out[k] = m
}
}
return out, nil
},
Manifest: func(digest string) (string, error) {
key, err := delivery.ManifestsKey(digest)
if err != nil {
return "", err
}
e, err := manifests.Get(key)
if err != nil {
return "", err
}
if e == nil {
return "", fmt.Errorf("the manifests state holds no %s", digest)
}
kept, err := delivery.DecodeManifest(e.Value)
return kept.JSON, err
},
Graph: func() (delivery.Graph, error) {
keys, err := graph.Keys()
if err != nil {
return nil, err
}
g := delivery.Graph{}
for _, k := range keys {
e, err := graph.Get(k)
if err != nil {
return nil, fmt.Errorf("graph.%s: %w", k, err)
}
if e == nil {
continue
}
if n, err := delivery.DecodeGraphNode(e.Value); err == nil {
g[k] = n
}
}
return g, nil
},
}
}
// catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat.
func catalogTools() []stdio.Tool {
return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(delivery.GraphState)))
}
@@ -0,0 +1,171 @@
package index
import (
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The seat mesh-catalog's verbs, answered from the indexer's own states (novox/hq ADR 0604 §1, §2): modules, module,
// provides and dependents read the modules state, the manifests state and the module graph, and write nothing. The
// indexer serves no request subject (ADR 0437 §4): its tools bundle serves them as <seat>.<verb>. Each answer keeps the
// field names of the old catalog module's tool it replaces (catalog_modules, catalog_module, catalog_provides,
// catalog_dependents), so a caller that moves to the seat reads the same fields.
// CatalogRead reads the states the seat's verbs answer from.
type CatalogRead struct {
// Modules is every value of the modules state, by module.
Modules func() (map[string]delivery.Module, error)
// Manifest is the manifest kept under a digest in the manifests state, as its JSON.
Manifest func(digest string) (string, error)
// Graph is the module graph, by module.
Graph func() (delivery.Graph, error)
}
// CatalogEntry is one module of the catalog: its repository, its directory there (delivery.RootPath for the root), the
// merge commit it was last changed at, and its fingerprint or why it has none.
type CatalogEntry struct {
Module string `json:"module"`
Repository string `json:"repository"`
Path string `json:"path"`
Commit string `json:"commit"`
FP string `json:"fp,omitempty"`
None string `json:"none,omitempty"`
}
// CatalogModule is one module whole: its entry and its manifest as the trunk holds it.
type CatalogModule struct {
CatalogEntry
Manifest json.RawMessage `json:"manifest"`
}
// CatalogEnd is a module that provides, requires or depends on something, with its commit; Scope is a provider's scope.
type CatalogEnd struct {
Module string `json:"module"`
Commit string `json:"commit"`
Scope string `json:"scope,omitempty"`
}
func entryOf(name string, m delivery.Module) CatalogEntry {
return CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2}
}
// CatalogModulesAnswer is the verb modules: every module the modules state holds, by name.
func (c CatalogRead) CatalogModulesAnswer() (map[string]any, error) {
mods, err := c.Modules()
if err != nil {
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
}
out := []CatalogEntry{}
for _, name := range slices.Sorted(maps.Keys(mods)) {
out = append(out, entryOf(name, mods[name]))
}
return map[string]any{"modules": out}, nil
}
// CatalogModuleAnswer is the verb module: one module whole, its manifest included.
func (c CatalogRead) CatalogModuleAnswer(name string) (map[string]any, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name the module")
}
mods, err := c.Modules()
if err != nil {
return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
}
m, ok := mods[name]
if !ok {
return nil, fmt.Errorf("the trunk holds no module %s", name)
}
one := CatalogModule{CatalogEntry: entryOf(name, m)}
if m.Manifest == "" {
return nil, fmt.Errorf("the indexer keeps no manifest of %s", name)
}
raw, err := c.Manifest(m.Manifest)
if err != nil {
return nil, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err)
}
one.Manifest = json.RawMessage(raw)
return map[string]any{"module": one}, nil
}
// CatalogProvidesAnswer is the verb provides: the modules that provide a provision, each with its scope (a module provides
// its own name, at node scope), and the modules that require it, from the module graph's provision edges.
func (c CatalogRead) CatalogProvidesAnswer(provision string) (map[string]any, error) {
provision = strings.TrimSpace(provision)
if provision == "" {
return nil, fmt.Errorf("name the provision")
}
g, mods, err := c.graphAndModules()
if err != nil {
return nil, err
}
provides, requires := []CatalogEnd{}, []CatalogEnd{}
for _, name := range g.Providers(provision) {
end := CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode}
if name != provision {
end.Scope = c.scopeOf(mods[name], provision)
}
provides = append(provides, end)
}
for _, name := range g.Requirers(provision) {
requires = append(requires, CatalogEnd{Module: name, Commit: mods[name].Commit})
}
return map[string]any{"provision": provision, "provides": provides, "requires": requires}, nil
}
// scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read.
func (c CatalogRead) scopeOf(m delivery.Module, provision string) string {
if m.Manifest == "" {
return ""
}
raw, err := c.Manifest(m.Manifest)
if err != nil {
return ""
}
man, err := catalogue.Decode([]byte(raw))
if err != nil {
return ""
}
for _, o := range man.Provides {
if o.Name == provision {
return o.At()
}
}
return ""
}
// CatalogDependentsAnswer is the verb dependents: the modules whose build edges reach the module, directly or through
// others, from the module graph.
func (c CatalogRead) CatalogDependentsAnswer(module string) (map[string]any, error) {
module = strings.TrimSpace(module)
if module == "" {
return nil, fmt.Errorf("name the module")
}
g, mods, err := c.graphAndModules()
if err != nil {
return nil, err
}
out := []CatalogEnd{}
for _, name := range g.Dependents(module) {
out = append(out, CatalogEnd{Module: name, Commit: mods[name].Commit})
}
return map[string]any{"module": module, "dependents": out}, nil
}
func (c CatalogRead) graphAndModules() (delivery.Graph, map[string]delivery.Module, error) {
g, err := c.Graph()
if err != nil {
return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err)
}
mods, err := c.Modules()
if err != nil {
return nil, nil, fmt.Errorf("the indexer's modules cannot be read: %w", err)
}
return g, mods, nil
}
@@ -0,0 +1,125 @@
package index
import (
"context"
"encoding/json"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// capturedKeys are the field names of the first object at path (a list's first element when the value is a list) in the
// old catalog module's answer captured with mesh_call on 2026-11-03 (testdata/catalog; trimmed to a few modules, and the
// manifest of catalog_module to three of its keys).
func capturedKeys(t *testing.T, file string, path ...string) []string {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "catalog", file))
if err != nil {
t.Fatal(err)
}
return keysAt(t, raw, path...)
}
func keysAt(t *testing.T, raw []byte, path ...string) []string {
t.Helper()
var v any
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatal(err)
}
for _, p := range path {
v = v.(map[string]any)[p]
if l, ok := v.([]any); ok {
v = l[0]
}
}
var out []string
for k := range v.(map[string]any) {
out = append(out, k)
}
slices.Sort(out)
return out
}
// catalogVerbsAnswerFromTheStates: the seat mesh-catalog's four verbs answer from the indexer's modules, manifests and
// graph, with the field names of the old catalog module's answers (ADR 0604 §1, §2).
func catalogVerbsAnswerFromTheStates(t *testing.T) {
withAcme := strings.Replace(renamed(t, "audit-logger", "acme"), `"module": "acme",`,
`"module": "acme", "provides": [{"name": "internal-acme-ca", "scope": "mesh", "answered-by": "helper"}],`, 1)
ix, mods := newFormIndexer(t, mod{"mesh-tools", renamed(t, "avahi", "mesh-tools")},
mod{"avahi", standingOn(t, catalogueManifest(t, "avahi"), "mesh-tools")},
mod{"ca-trust", catalogueManifest(t, "ca-trust")}, mod{"acme", withAcme})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
c := CatalogRead{
Modules: func() (map[string]delivery.Module, error) {
out := map[string]delivery.Module{}
for k := range mods.vals {
m, _ := moduleOf(t, mods, k)
out[k] = m
}
return out, nil
},
Manifest: func(digest string) (string, error) {
raw, _, err := ix.manifestRaw(context.Background(), digest)
return raw, err
},
Graph: func() (delivery.Graph, error) {
g := delivery.Graph{}
for k := range ix.Graph.(*memStore).vals {
g[k] = graphOf(t, ix, k)
}
return g, nil
},
}
answer := func(v map[string]any, err error) []byte {
t.Helper()
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(v)
return raw
}
contains := func(got, want []string) bool {
for _, w := range want {
if !slices.Contains(got, w) {
return false
}
}
return true
}
raw := answer(c.CatalogModulesAnswer())
if got, want := keysAt(t, raw, "modules"), capturedKeys(t, "catalog_modules.json", "modules"); !contains(got, want) {
t.Errorf("modules' fields %v, want the old catalog's %v", got, want)
}
if !strings.Contains(string(raw), `"module":"avahi"`) || !strings.Contains(string(raw), `"fp":"src2:`) {
t.Errorf("modules = %s", raw)
}
raw = answer(c.CatalogModuleAnswer("ca-trust"))
want := slices.DeleteFunc(capturedKeys(t, "catalog_module.json", "module"), func(k string) bool { return k == "ref" }) // the trunk's, no ref
if got := keysAt(t, raw, "module"); !contains(got, want) || !strings.Contains(string(raw), `"internal-acme-ca"`) {
t.Errorf("module ca-trust = %s, want the fields %v and its manifest", raw, want)
}
if _, err := c.CatalogModuleAnswer("gone"); err == nil || !strings.Contains(err.Error(), "no module gone") {
t.Errorf("a module the trunk does not hold: %v", err)
}
raw = answer(c.CatalogProvidesAnswer("internal-acme-ca"))
if got, want := keysAt(t, raw, "provides"), capturedKeys(t, "catalog_provides.json", "provides"); !reflect.DeepEqual(got, want) ||
!reflect.DeepEqual(keysAt(t, raw, "requires"), capturedKeys(t, "catalog_provides.json", "requires")) ||
!strings.Contains(string(raw), `"provides":[{"module":"acme","commit":"`+merge+`","scope":"mesh"}]`) ||
!strings.Contains(string(raw), `"requires":[{"module":"ca-trust"`) {
t.Errorf("provides internal-acme-ca = %s", raw)
}
raw = answer(c.CatalogDependentsAnswer("mesh-tools"))
if got, want := keysAt(t, raw), capturedKeys(t, "catalog_dependents.json"); !reflect.DeepEqual(got, want) ||
!strings.Contains(string(raw), `"dependents":[{"module":"avahi"`) {
t.Errorf("dependents of mesh-tools = %s", raw)
}
raw = answer(c.CatalogDependentsAnswer("mesh-build-go")) // the build module reaches every Go module it builds
if !strings.Contains(string(raw), `"avahi"`) || !strings.Contains(string(raw), `"mesh-tools"`) {
t.Errorf("dependents of mesh-build-go = %s", raw)
}
}
@@ -45,6 +45,7 @@ func withManifests(f delivery.Forge, manifests map[string]string, locks map[stri
func TestAMergedManifestIsKeptOnceByItsDigestAndNamedByTheModule(t *testing.T) {
t.Run("kept by its digest and named by the module", mergedManifestIsKeptByItsDigestAndNamedByTheModule)
t.Run("the same manifest applied again is written once", sameManifestAppliedAgainIsWrittenOnce)
t.Run("the seat mesh-catalog's verbs answer from the states", catalogVerbsAnswerFromTheStates)
}
func mergedManifestIsKeptByItsDigestAndNamedByTheModule(t *testing.T) { // 1449/AC1
@@ -0,0 +1,4 @@
{
"dependents": [],
"module": "mesh-build-go"
}
@@ -0,0 +1,18 @@
{
"module": {
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
"manifest": {
"module": "gitea",
"requires": [
"postgres-database",
"route",
"secret"
],
"version": "1"
},
"module": "gitea",
"path": "modules/gitea",
"ref": "refs/pull/571/head",
"repository": "novox/mesh-catalog"
}
}
@@ -0,0 +1,22 @@
{
"modules": [
{
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
"module": "gitea",
"path": "modules/gitea",
"repository": "novox/mesh-catalog"
},
{
"commit": "dc90b3f5bd1386747ac70f2792169babf8eeed73",
"module": "mesh-controller",
"path": ".",
"repository": "novox/mesh-controller"
},
{
"commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d",
"module": "postgres",
"path": "modules/postgres",
"repository": "novox/mesh-catalog"
}
]
}
@@ -0,0 +1,15 @@
{
"provides": [
{
"commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d",
"module": "postgres",
"scope": "mesh"
}
],
"requires": [
{
"commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026",
"module": "gitea"
}
]
}