package graph import ( "encoding/json" "errors" "fmt" "maps" "reflect" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Version is the version of a node's value this package writes and the only one it reads. const Version = 1 // ErrUnknownVersion is what Decode returns for a value whose version is missing or not Version. var ErrUnknownVersion = errors.New("graph: unknown value version") func decode(data []byte, into *Node) error { var probe struct { V *int `json:"v"` } if err := json.Unmarshal(data, &probe); err != nil { return fmt.Errorf("graph: value does not fit its shape: %w", err) } if probe.V == nil || *probe.V != Version { return fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version) } if err := json.Unmarshal(data, into); err != nil { return fmt.Errorf("graph: value does not fit its shape: %w", err) } return nil } // checkName refuses a module name a key cannot hold (delivery.ModulesKey's rule). func checkName(kind, name string) error { if _, err := delivery.ModulesKey(name); err != nil { return fmt.Errorf("graph: the %s name %q: %w", kind, name, err) } return nil } // checkRepository refuses a repository that is not /. func checkRepository(repository string) error { owner, name, ok := strings.Cut(repository, "/") if !ok || owner == "" || name == "" || strings.Contains(name, "/") { return fmt.Errorf("graph: the repository %q is not /", repository) } return nil } // The module graph (novox/hq ADR 0603 §2, ADR 0604 §1): the indexer keeps, as its state `graph`, one key per module it // holds the inputs of, written at every recompute and deleted with the module. A key holds the module's build edges // (its bases, the mesh packages its fingerprint takes, and per language the build module, toolchain version and recipe // version its fingerprint names, ADR 0530 §1) and its provision edges (what its manifest provides, requires, and of what // it requires what a want of it waits for). Nothing stores an order: a reader works it out from the edges (ADR 0603 // §1, §6). The indexer writes it and works the edges out (EdgesOf), the delivery view, the checker and the indexer's // answers of the seat mesh-catalog read it; all three are in this repository, which uses this package by path (the // operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts). It names no module. const ( // State is the name of the indexer's state of the module graph, as its manifest declares it, and Bucket its // bucket: one key per module (Key). State = "graph" Bucket = delivery.IndexerModule + "_" + State // Read is the line a reader's manifest names in `reads` to read the graph. Read = delivery.IndexerModule + "." + State // History is the revisions the graph state keeps per key (ADR 0603 §2). History = 5 ) // Key is the key, in Bucket, of a module: ``, as ModulesKey. func Key(module string) (string, error) { return delivery.ModulesKey(module) } // ParseKey is the inverse of Key. func ParseKey(key string) (string, error) { module, err := Key(key) if err != nil { return "", fmt.Errorf("graph: %q is not a graph key: %w", key, err) } return module, nil } // Node is the value of a graph key: one module's edges as its last recompute computed them. FP is its new-form // fingerprint then, or None why it has none (exactly one is set); Repository and Path say where it is (as Module), so a // package edge is resolved to the module that publishes it (GraphPublisher). Bases are the modules it is built on; // Packages the mesh packages (Go module paths) its fingerprint took (Module.Packages); Toolchains, by language, the // build module and the toolchain it was computed with. Provides are the provisions its manifest offers (its own name is // provided too and is not listed), Requires what it requires and does not require optionally, and Awaits those of // Requires a want of it waits for (catalogue.Awaited, ADR 0570 §4): the provision edges that order a source revision. // A bus-state entry of its manifest is a requirement of the provision BusState (ADR 0614 §5). Invokes are the seat verbs // (`seat:.`) and module tools (`.`) its manifest and its own bus accounts invoke, Serves those // its claims serve and its own tools answer, in the same two forms (ADR 0614 §2), and LeftOut those of Invokes whose // verb edge lies on a cycle and is left out of the graph, so no want waits on it (§3; LeaveOut works them out). // Every list is sorted and each entry once; an encode sorts them. type Node struct { V int `json:"v"` FP string `json:"fp,omitempty"` None string `json:"none,omitempty"` Repository string `json:"repository"` Path string `json:"path,omitempty"` Bases []string `json:"bases,omitempty"` Packages []string `json:"packages,omitempty"` Toolchains map[string]Toolchain `json:"toolchains,omitempty"` Provides []string `json:"provides,omitempty"` Requires []string `json:"requires,omitempty"` Awaits []string `json:"awaits,omitempty"` Invokes []string `json:"invokes,omitempty"` Serves []string `json:"serves,omitempty"` LeftOut []string `json:"left-out,omitempty"` } // BusState is the provision a bus-state entry of a manifest requires (ADR 0614 §5): the bus module's build that offers it // goes before the module that asks bus state. const BusState = "bus-state" // Toolchain is a build edge of one language: the build module that builds it (Module) and the toolchain version // and recipe version its fingerprint names. type Toolchain struct { Module string `json:"module"` Version string `json:"version"` Recipe string `json:"recipe"` } func (g Node) check() error { switch { case (g.FP == "") == (g.None == ""): return fmt.Errorf("graph: a graph node has either a fingerprint or a reason for none, not both or neither") case g.FP != "" && !strings.HasPrefix(g.FP, delivery.SourcePrefix2): return fmt.Errorf("graph: the graph node's fingerprint %q does not start with %s", g.FP, delivery.SourcePrefix2) } if err := checkRepository(g.Repository); err != nil { return err } for _, b := range g.Bases { if err := checkName("base", b); err != nil { return err } } for _, p := range g.Packages { if _, _, ok := delivery.PackageRepository(p); !ok { return fmt.Errorf("graph: the graph node's package %q is no mesh package's path", p) } } for language, t := range g.Toolchains { if err := checkName("build module", t.Module); err != nil { return fmt.Errorf("graph: the graph node's toolchain of %q: %w", language, err) } if language == "" || t.Version == "" || t.Recipe == "" { return fmt.Errorf("graph: the graph node's toolchain of %q needs its language, version and recipe version", language) } } for _, list := range [][]string{g.Provides, g.Requires, g.Awaits} { if slices.Contains(list, "") { return fmt.Errorf("graph: a graph node names an empty provision") } } for _, a := range g.Awaits { if !slices.Contains(g.Requires, a) { return fmt.Errorf("graph: the graph node awaits %q, which it does not require", a) } } for _, list := range [][]string{g.Invokes, g.Serves} { for _, v := range list { if !isVerb(v) { return fmt.Errorf("graph: the graph node names %q, which is neither seat:. nor .", v) } } } for _, l := range g.LeftOut { if !slices.Contains(g.Invokes, l) { return fmt.Errorf("graph: the graph node leaves out %q, which it does not invoke", l) } } return nil } // isVerb says whether v is a seat verb `seat:.` or a module tool `.`. func isVerb(v string) bool { if _, _, ok := catalogue.SeatInvoke(v); ok { return true } module, tool, ok := strings.Cut(v, ".") return ok && module != "" && tool != "" && !strings.HasPrefix(v, "seat:") } func sortedOnce(in []string) []string { if len(in) == 0 { return nil } out := slices.Clone(in) slices.Sort(out) return slices.Compact(out) } func (g Node) normal() Node { g.Bases, g.Packages = sortedOnce(g.Bases), sortedOnce(g.Packages) g.Provides, g.Requires, g.Awaits = sortedOnce(g.Provides), sortedOnce(g.Requires), sortedOnce(g.Awaits) g.Invokes, g.Serves, g.LeftOut = sortedOnce(g.Invokes), sortedOnce(g.Serves), sortedOnce(g.LeftOut) if len(g.Toolchains) == 0 { g.Toolchains = nil } return g } // Encode is g as JSON at Version, its lists sorted; it refuses what Node's contract does not hold. func Encode(g Node) ([]byte, error) { g = g.normal() if err := g.check(); err != nil { return nil, err } g.V = Version return json.Marshal(g) } // Decode reads a graph node; it refuses an unknown version (ErrUnknownVersion) and what Encode refuses. func Decode(data []byte) (Node, error) { var g Node if err := decode(data, &g); err != nil { return Node{}, err } if err := g.check(); err != nil { return Node{}, err } return g.normal(), nil } // Same says whether two graph nodes are the same value once sorted, so a writer writes only when they differ. func Same(a, b Node) bool { a, b = a.normal(), b.normal() a.V, b.V = 0, 0 return reflect.DeepEqual(a, b) } // Graph is the module graph as a reader holds it: every graph node, by module. type Graph map[string]Node // Providers are the modules that provide provision: each whose Provides names it, and the module of that name (a module // provides its own name), sorted. func (g Graph) Providers(provision string) []string { var out []string for name, n := range g { if name == provision || slices.Contains(n.Provides, provision) { out = append(out, name) } } slices.Sort(out) return out } // Requirers are the modules that require provision, not optionally, sorted. func (g Graph) Requirers(provision string) []string { var out []string for name, n := range g { if slices.Contains(n.Requires, provision) { out = append(out, name) } } slices.Sort(out) return out } // Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository): // the module whose directory is the package's directory or the nearest one above it (the longest match, a package // published from inside a module's directory belonging to that module), or "" when the graph holds none. func (g Graph) Publisher(pkg string) string { repository, dir, ok := delivery.PackageRepository(pkg) if !ok { return "" } if dir == "" { dir = delivery.RootPath } best, bestLen := "", -1 for _, name := range slices.Sorted(maps.Keys(g)) { n := g[name] if n.Repository != repository { continue } switch { case n.Path == dir: return name case n.Path == delivery.RootPath: if bestLen < 0 { best, bestLen = name, 0 } case strings.HasPrefix(dir, n.Path+"/") && len(n.Path) > bestLen: best, bestLen = name, len(n.Path) } } return best } // buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build // module of each language. func (g Graph) buildEdges(n Node) []string { out := slices.Clone(n.Bases) for _, p := range n.Packages { if m := g.Publisher(p); m != "" { out = append(out, m) } } for _, t := range n.Toolchains { out = append(out, t.Module) } return sortedOnce(out) } // Dependents are the modules whose build edges reach module, directly or through other modules' (a base of a base, a // module built by a build module that stands on module), sorted and without module itself (ADR 0604 §1): what a // change of module builds again. func (g Graph) Dependents(module string) []string { reverse := map[string][]string{} for _, name := range slices.Sorted(maps.Keys(g)) { for _, to := range g.buildEdges(g[name]) { reverse[to] = append(reverse[to], name) } } seen := map[string]bool{module: true} queue := []string{module} var out []string for len(queue) > 0 { at := queue[0] queue = queue[1:] for _, from := range reverse[at] { if !seen[from] { seen[from] = true out = append(out, from) queue = append(queue, from) } } } slices.Sort(out) return out } // Servers are the modules whose Serves names invoke (a seat verb or a module tool), sorted. func (g Graph) Servers(invoke string) []string { var out []string for name, n := range g { if slices.Contains(n.Serves, invoke) { out = append(out, name) } } slices.Sort(out) return out } // waits are the modules name waits for, sorted: its bases, for each provision it awaits every other module that provides // it (ADR 0603 §1), and with verbs for each invoke the module serving it (ADR 0614 §2), those it leaves out (§3) left // out unless all is set. func (g Graph) waits(name string, verbs, all bool) []string { n := g[name] out := slices.Clone(n.Bases) for _, a := range n.Awaits { for _, p := range g.Providers(a) { if p != name { out = append(out, p) } } } if verbs { for _, v := range n.Invokes { if !all && slices.Contains(n.LeftOut, v) { continue } for _, s := range g.Servers(v) { if s != name || all { out = append(out, s) } } } } return sortedOnce(out) } // Before are the modules name waits for, by the graph's ordering edges, sorted: its bases, for each provision it awaits // (bus state among them, ADR 0614 §5) every other module that provides it (ADR 0603 §1), and for each verb it invokes // and the graph keeps, every other module that serves it (ADR 0614 §2, §3). A toolchain or package edge orders nothing: // it is a trigger, never a wait (ADR 0614 §6). func (g Graph) Before(name string) []string { return g.waits(name, true, false) } // LeaveOut sets each node's LeftOut (ADR 0614 §3): every invoke whose verb edge lies on a cycle of the edges a part waits // on (bases, awaited provisions, every verb edge), whichever module the edge starts from, a module invoking what it // serves itself included. An edge lies on a cycle when the module it reaches reaches the module it starts from: both are // in one strongly connected part of the graph. What is left of the graph then has no cycle through a verb edge, so the // check's refusal (Cycle) never meets one. func (g Graph) LeaveOut() { part := g.parts(func(name string) []string { return g.waits(name, true, true) }) for _, name := range slices.Sorted(maps.Keys(g)) { n := g[name] n.LeftOut = nil for _, v := range n.Invokes { for _, s := range g.Servers(v) { if part[s] == part[name] { n.LeftOut = append(n.LeftOut, v) break } } } g[name] = n.normal() } } // parts numbers each module by the strongly connected part of edges it is in (Tarjan's), so two modules have one number // when each reaches the other. func (g Graph) parts(edges func(string) []string) map[string]int { index, low, part := map[string]int{}, map[string]int{}, map[string]int{} onStack := map[string]bool{} var stack []string next, parts := 0, 0 var visit func(string) visit = func(name string) { index[name], low[name] = next, next next++ stack = append(stack, name) onStack[name] = true for _, to := range edges(name) { if _, ours := g[to]; !ours { continue } if _, seen := index[to]; !seen { visit(to) low[name] = min(low[name], low[to]) } else if onStack[to] { low[name] = min(low[name], index[to]) } } if low[name] == index[name] { for { top := stack[len(stack)-1] stack = stack[:len(stack)-1] onStack[top] = false part[top] = parts if top == name { break } } parts++ } } for _, name := range slices.Sorted(maps.Keys(g)) { if _, seen := index[name]; !seen { visit(name) } } return part } // Cycle is the first cycle of the graph's ordering edges (Before), as the modules it passes through with the first named // again at its end (a -> b -> a), or nil for none (ADR 0603 §2: the check refuses it and names it). The ordering edges // are what a part waits on, provisions, bus state and the verb edges the graph keeps (ADR 0614 §6), and bases, since a // module built on another's artifact can never build in a cycle of bases: modules ordered on each other would each wait // for the other for good. A verb edge the graph leaves out (LeaveOut) is none, and neither is a toolchain edge (ADR 0603 // §3: nothing waits on a build module) or a package edge (ADR 0614 §6: a trigger). The modules are visited in sorted // order, so the same graph names the same cycle. func (g Graph) Cycle() []string { edges := g.Before const ( unseen = iota onPath done ) state := map[string]int{} var path []string var visit func(string) []string visit = func(name string) []string { state[name] = onPath path = append(path, name) for _, to := range edges(name) { if _, ours := g[to]; !ours { continue } switch state[to] { case onPath: i := slices.Index(path, to) return append(slices.Clone(path[i:]), to) case unseen: if c := visit(to); c != nil { return c } } } path = path[:len(path)-1] state[name] = done return nil } for _, name := range slices.Sorted(maps.Keys(g)) { if state[name] == unseen { if c := visit(name); c != nil { return c } } } return nil } // EdgesOf is the graph node of manifest's own edges: its bases (each module its build stands on), what it provides, // what it requires and does not require optionally, a bus-state entry as a requirement of BusState (ADR 0614 §5), and of // those what a want of it waits for (catalogue.Awaited, as the assigner judges it; BusState whenever a provider offers // it), providers answering the manifests that provide a provision; and the verbs and tools it invokes and serves // (InvokesOf, ServesOf). The fingerprint, place, packages, toolchains and the verb edges left out (LeaveOut, over the // whole graph) are the indexer's to add; this is the part a reader of manifests alone (the checker) works out the same way. func EdgesOf(manifest catalogue.Manifest, providers func(provision string) []catalogue.Manifest) Node { var n Node if manifest.Build != nil { for _, on := range manifest.Build.On { if on.Module != "" { n.Bases = append(n.Bases, on.Module) } } } for _, o := range manifest.Provides { n.Provides = append(n.Provides, o.Name) } for _, r := range manifest.Requires { if !manifest.OptionalRequires[r] { n.Requires = append(n.Requires, r) } } n.Awaits = catalogue.Awaited(manifest, providers) if len(manifest.BusState) > 0 { n.Requires = append(n.Requires, BusState) if len(providers(BusState)) > 0 { n.Awaits = append(n.Awaits, BusState) } } n.Invokes, n.Serves = InvokesOf(manifest), ServesOf(manifest) return n.normal() } // InvokesOf are the seat verbs and module tools manifest invokes, its own and its processes' and tools bundles' bus // accounts' (catalogue.Manifest.BusAccounts), sorted, each once; `*`, every tool, names no edge and is left out. func InvokesOf(manifest catalogue.Manifest) []string { all := slices.Clone(manifest.Invokes) for _, a := range manifest.BusAccounts() { all = append(all, a.Bus.Invokes...) } var out []string for _, v := range all { if isVerb(v) { out = append(out, v) } } return sortedOnce(out) } // ServesOf are the seat verbs manifest's claims serve (`seat:.`, catalogue.Claim.ServesFor) and its own // tools (`.`), sorted, each once. func ServesOf(manifest catalogue.Manifest) []string { var out []string for _, c := range manifest.Claims { for _, v := range c.ServesFor(manifest) { out = append(out, "seat:"+c.Name+"."+v) } } for _, t := range manifest.Tools { out = append(out, manifest.Module+"."+t) } var verbs []string for _, v := range out { if isVerb(v) { verbs = append(verbs, v) } } return sortedOnce(verbs) } // OfManifests is the graph of these manifests' own edges (EdgesOf), by module, each provision's providers being // the manifests among them that offer it, with the verb edges on a cycle left out (LeaveOut): what the check works out // for a change to find a cycle (ADR 0603 §2, ADR 0614 §3). func OfManifests(manifests map[string]catalogue.Manifest) Graph { providers := func(provision string) []catalogue.Manifest { var out []catalogue.Manifest for _, name := range slices.Sorted(maps.Keys(manifests)) { if slices.Contains(manifests[name].Offers(), provision) { out = append(out, manifests[name]) } } return out } g := Graph{} for name, m := range manifests { g[name] = EdgesOf(m, providers) } g.LeaveOut() return g } // Served is whether a running build serves a seat verb or module tool (ADR 0614 §2, §7), counted from the holders' own // haves as a provider is (ADR 0603 §5): every have running healthy, by node and module, whose build's manifest serves it // (servesOf answers the verbs of a module's build at a fingerprint, ServesOf of its manifest, and whether it was read). // A build whose manifest cannot be read counts what fallback answers of its module (the graph's Serves, so a build read // late never holds its callers for good, or nothing), and is named in unread, sorted, so its reader says so. func Served(haves map[string]map[string]delivery.Have, servesOf func(module, fp string) ([]string, bool), fallback func(module string) []string) (served func(invoke string) bool, unread []string) { set := map[string]bool{} for _, node := range slices.Sorted(maps.Keys(haves)) { for _, module := range slices.Sorted(maps.Keys(haves[node])) { h := haves[node][module] if h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthOK { continue } serves, ok := servesOf(module, h.FP) if !ok { unread = append(unread, module) if fallback != nil { serves = fallback(module) } } for _, v := range serves { set[v] = true } } } return func(v string) bool { return set[v] }, slices.Compact(slices.Sorted(slices.Values(unread))) }