Files
mesh-delivery/modules/mesh-delivery-checker/rules/graph.go
T
jschoubben bd20ba5b8f Checker: the build modules are the holders of the build seats on the seat list, never a name
The checker's fixed names of the build modules (buildmodules.go in its program, rules and check) go: the
Examiner finds each build module by the language its seat's acting rule builds (buildrun.BuildSeats over
the seat list's definitions and the holders the indexer's modules name by their manifests' claims), the facts
carry the language where a level counts by it, and the program's manifest reads the steps and toolchains of
the seats and hears their toolchain-reported. The tools read the steps of each holder and follow the seat
list as the view does. Timers stay (A-118).

Builds: design 47 G A-82, A-118, §204
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 01:31:24 +01:00

115 lines
3.8 KiB
Go

package rules
import (
"fmt"
"slices"
"strings"
)
// The module graph's cycle rule (novox/hq ADR 0603 §2, ADR 0614 §3, §6), judged by the checking program (ADR 0656 §5):
// the check refuses a change whose module graph has a cycle and names it; a cycle the trunk already has among the same
// modules is the trunk's, said in the notes. The running checker gathers the graph's ordering edges of the catalogue as
// the change leaves it and as the trunk has it, worked out with the contract's graph (sdk/graph: bases, the provisions
// and bus state a module awaits, the verb edges the graph keeps), as module names and the edge's kind, never the
// manifests themselves, so the facts stay within the run input's bound.
// The kinds of an ordering edge.
const (
EdgeBase = "base"
EdgeAwaits = "awaits"
EdgeInvokes = "invokes"
)
// Edge is one ordering edge: From waits for To. Only is EdgeChange when only the change's graph has it, EdgeTrunk when
// only the trunk's does, empty when both have it.
type Edge struct {
From string `json:"from"`
To string `json:"to"`
Kind string `json:"kind"`
Only string `json:"only,omitempty"`
}
// Where only one graph has an edge.
const (
EdgeChange = "change"
EdgeTrunk = "trunk"
)
// Graph is the module graph's facts: every ordering edge of either graph, between modules of the catalogue. Trunk says
// whether the trunk's graph was read; when it was not, every edge is the change's and no cycle is the trunk's.
type Graph struct {
Edges []Edge `json:"edges"`
Trunk bool `json:"trunk"`
}
// JudgeGraph is the cycle rule over the graph's facts: the failure of a cycle the change makes, or the note of no cycle
// or of a cycle the trunk already has.
func JudgeGraph(g Graph, machine string) (*FailedFirst, []string) {
cycle := firstCycle(g.Edges, func(e Edge) bool { return e.Only != EdgeTrunk || !g.Trunk })
if cycle == nil {
return nil, []string{"the change's module graph has no cycle"}
}
words := strings.Join(cycle, " -> ")
if g.Trunk {
among := firstCycle(g.Edges, func(e Edge) bool {
return e.Only != EdgeChange && slices.Contains(cycle, e.From) && slices.Contains(cycle, e.To)
})
if among != nil {
return nil, []string{"the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)"}
}
}
return &FailedFirst{Machine: machine, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+
"each wait for the other for good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words)}, nil
}
// firstCycle is the first cycle of the edges kept, as the modules it passes through with the first named again at its
// end, visiting the modules and each one's edges in sorted order (as sdk/graph's Cycle does), or nil for none.
func firstCycle(edges []Edge, keep func(Edge) bool) []string {
next := map[string][]string{}
for _, e := range edges {
if keep(e) && !slices.Contains(next[e.From], e.To) {
next[e.From] = append(next[e.From], e.To)
}
}
var names []string
for name, to := range next {
slices.Sort(to)
names = append(names, name)
}
slices.Sort(names)
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 next[name] {
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 names {
if state[name] == unseen {
if c := visit(name); c != nil {
return c
}
}
}
return nil
}