B0's shape wins: the running checker asks and records facts, the checking program decides. The registry's types win: the rules and the program read the generated checker contract and the domain SDK, and no mesh-sdk delivery import is left. The registry's series, renames and unparsed seat definitions become facts with their refusals as rules; the tagged versions ride on the case. The program reads the heads' seat definitions through sdk/seatdefs and the running checker lays them over its reader's list by seat, reading no file of the heads: the reader's list is larger than a run input's bound. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package rules
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
)
|
|
|
|
// 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) (*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: 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
|
|
}
|