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. // 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"` } // 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) } } return nil } 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) 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 } // Before are the modules name waits for, by the graph's ordering edges, sorted: its bases and, for each provision it // awaits, every other module that provides it (ADR 0603 §1). A toolchain or package edge orders nothing (§3). func (g Graph) Before(name string) []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) } } } return sortedOnce(out) } // Cycle is the first cycle of the graph's ordering edges, 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 a // module's bases and, for each provision it awaits, every other module that provides it: modules ordered on each other // would each wait for the other for good. A toolchain edge is no ordering edge (ADR 0603 §3: nothing waits on a build // module), and neither is a package edge. 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, and of those what a want of it waits for (catalogue.Awaited, as the // assigner judges it), providers answering the manifests that provide a provision. The fingerprint, place, packages and // toolchains 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) return n.normal() } // OfManifests is the graph of these manifests' own edges (EdgesOf), by module, each provision's providers being // the manifests among them that offer it: what the check works out for a change to find a cycle (ADR 0603 §2). 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) } return g }