The indexer writes each module's verb edges into the module graph, the ones on a cycle left out over the whole graph (ADR 0614 §2, §3; issue 774)

The assigner reads which edges are left out from the graph, so one place decides the cycle; the whole graph is
judged, a module past the graph's cap included, before the first GraphMost are written.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 23:25:35 +01:00
parent df05b1a5f7
commit b4bfc14556
2 changed files with 29 additions and 3 deletions
@@ -15,8 +15,8 @@ import (
)
// The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph
// (graph.Bucket), one key per module it holds the inputs of, its build and provision edges as that recompute
// computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value
// (graph.Bucket), one key per module it holds the inputs of, its build, provision and verb edges as that recompute
// computed them (ADR 0614 §1), the verb edges on a cycle left out over the whole graph (§3), and deletes the key of a module the modules state no longer holds. A key is written only when its value
// changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat
// mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1).
@@ -89,13 +89,20 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
}
return out
}
// The verb edges on a cycle are left out over the whole graph (ADR 0614 §3), every module it knows, so a module past
// GraphMost still closes the cycle it lies on.
whole := graph.Graph{}
for name, e := range edges {
whole[name] = graphNode(e, providers)
}
whole.LeaveOut()
for _, name := range names {
key, err := graph.Key(name)
if err != nil {
ix.logf("graph.%s is not written: %v", name, err)
continue
}
next := graphNode(edges[name], providers)
next := whole[name]
err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
if cur, err := graph.Decode(value); err == nil && graph.Same(cur, next) {
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"time"
@@ -330,6 +331,24 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
t.Errorf("%s moved with a build of build-agent: fp2 = %q, want %s", name, m.FP2, fp)
}
}
// ADR 0614 §2, §3: the graph names what each module invokes and serves, and leaves out, over the whole graph, the
// verb edges of two modules calling each other, from either side; a third calling one of them keeps its edge.
invoking := func(name, invokes string) mod {
raw := renamed(t, "avahi", name)
return mod{name, strings.Replace(raw, `"version": "1",`, `"version": "1", "invokes": [`+invokes+`],`, 1)}
}
vx, _ := newFormIndexer(t, invoking("caller", `"callee.avahi_status"`), invoking("callee", `"caller.avahi_browse", "*"`),
invoking("third", `"callee.avahi_status"`))
for name, want := range map[string][2][]string{
"caller": {{"callee.avahi_status"}, {"callee.avahi_status"}},
"callee": {{"caller.avahi_browse"}, {"caller.avahi_browse"}},
"third": {{"callee.avahi_status"}, nil},
} {
if g := graphOf(t, vx, name); !reflect.DeepEqual(g.Invokes, want[0]) || !reflect.DeepEqual(g.LeftOut, want[1]) ||
!slices.Contains(g.Serves, name+".avahi_status") {
t.Errorf("graph.%s = %+v, want it to invoke %v and leave out %v", name, g, want[0], want[1])
}
}
}
func TestABuildHeardBeforeAnyToolchainIsReadDropsNoFingerprint(t *testing.T) { // 1727 (review: a restart flapped every FP2)