ADR 0656 §5 has every check judged by parts built from the change's heads, and no record keeps the cycle rule in the running checker. The facts carry each ordering edge's two modules and kind, never manifests: the whole catalogue's graph is 50 edges, 3195 bytes. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
118 lines
3.9 KiB
Go
118 lines
3.9 KiB
Go
package rules
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// 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) (*delivery.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 &delivery.FailedFirst{Machine: buildrun.GoBuildModule, 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
|
|
}
|