Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/beside.go
T
jschoubben df56af0765 Every check composes with the composer built at the node-engine member's head, else at its trunk, and a change's manifests carry the check's own judge's verdicts (issue 1921, milestone 47/G D12, D13)
The parked composition-at-running path is folded as the lead's ruling Q3: one
path, so no check composes with the running engines' own code (ADR 0656 §5).
The sandbox request no longer drops a change's manifest verdict: it carries the
check's judge's, or none, which the composer reads as accepted (ADR 0578 §3).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 03:00:35 +01:00

219 lines
9.1 KiB
Go

package check
import (
"context"
"errors"
"fmt"
"slices"
"strconv"
"strings"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR
// 0484 clause 4). Ref names the pull request's head when the commit is only there.
type BesideRef struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
Ref string `json:"ref,omitempty"`
// atTrunk says the sibling was read at its trunk's head, so a move of that trunk puts the verdict out of date and is
// carried (novox/hq ADR 0548 §6). Never sent: the node-engine at the commit the mesh runs, mesh-sdk at a pin and a
// group's member at its head do not move with a trunk.
atTrunk bool
// runs says the sibling was read at the commit the mesh runs (the node-engine, the indexer's record of mesh-host): a new
// running commit puts the verdict out of date and is carried (ADR 0548 §6). Never sent.
runs bool
}
// NodeEngineModule is the module whose running commit the node-engine sibling is read at.
const NodeEngineModule = nodeEngineModule
const (
catalogueRepository = "novox/mesh-catalog"
nodeEngineRepo = NodeEngineRepository
nodeEngineModule = "mesh-host"
// networkingRepository holds route-proxy, whose manifest the node-engine's proofs of a composition read.
networkingRepository = "novox/mesh-networking"
// contractsRepository and deliveryRepository hold the seat definitions mesh-sdk's bootstrapped seats are compared with: its
// tests read both beside it (beside.Dir "mesh-contracts" and "mesh-delivery"), at the group's head for a member, else at the
// trunk (a group try lays them out the same way: besideFor serves the check and the try).
contractsRepository = "novox/mesh-contracts"
deliveryRepository = DeliveryRepository
)
// besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside
// beside.go): the catalogue, the node-engine, the SDK, and the delivery's pinned forge dependencies (mesh-catalog
// checks/deps). Which commit of each is besideCommit's rule. No repository's tests read the controller, which ended by its
// unassignment (novox/hq ADR 0590, issue 1126), and its repository is emptied and archived (ADR 0593 §11), so it has no
// siblings placed either. mesh-sdk reads mesh-contracts and mesh-delivery (its bootstrapped seats against their contracts files). The catalogue's tests read the node-engine since its composer command moved there (ADR 0593
// §7): mesh-build-go builds it for a compose ask, and with it every repository its composer requires at the zero version
// (issue 1921: with mesh-sdk alone beside, that build looked them up on the network and failed): those are not named
// here but derived from each sibling's go.mod (zeroVersionSiblings), so no rule can lack one. The node-engine's read
// mesh-networking since route-proxy's proofs of a composition moved into them (ADR 0593).
var besideRules = map[string][]string{
nodeEngineRepo: {catalogueRepository, SDKRepository, networkingRepository},
SDKRepository: {catalogueRepository, nodeEngineRepo, contractsRepository, deliveryRepository},
catalogueRepository: {SDKRepository, nodeEngineRepo},
"novox/mesh-delivery": {SDKRepository, catalogueRepository},
}
// besideFor are the siblings a pull request's tests read (ADR 0484 clause 4): the node-engine at the head of the group's pull
// request for it when the group holds one, else at the commit the mesh runs (the commit of the indexer's module mesh-host,
// its trunk head when none is known); any other sibling, the catalogue
// included, at the head of the group's pull request for it when the group holds one, else at its trunk, since a group's
// check judges each member against the group's heads (ADR 0482 §2, ADR 0573 §4; issue 1126: a judge a group changed was
// judged against the catalogue's trunk). And first, the change's own repository at the trunk it is merged onto, so a test
// comparing a shrink-only list with the trunk's copy reads it at $MESH_CHECK_BESIDE/<name> (issue 1887 A12, ADR 0529 §2):
// that is where a catalogue change's own trunk copy is placed.
func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef, error) {
var heads map[string]string
if j.Trunks != nil {
heads = j.Trunks.Heads()
}
var out []BesideRef
if p.Trunk != "" {
out = append(out, BesideRef{Repository: p.Repository, Commit: p.Trunk})
}
place := func(repo string) {
switch {
case repo == nodeEngineRepo && c.member(repo) != nil:
// A group's check judges each member against the group's heads (ADR 0573 §4, ADR 0482 §2), and the group's
// node-engine is what runs after its merge: it is placed at the member's head, as any member is. Neither atTrunk
// nor runs, so no move of the trunk or of the running commit puts the verdict out of date (besideAtTrunk).
member := c.member(repo)
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)})
case repo == nodeEngineRepo:
commit := ""
if j.Runs != nil {
commit = j.Runs.CommitOf(nodeEngineModule)
}
atTrunk := false
if commit == "" {
commit, atTrunk = heads[repo], true
}
if commit != "" {
// Named in the verdict either way (ADR 0548 §6, issue 568's own example): at the commit the mesh runs, a new
// running commit puts the verdict out of date; at the trunk, a trunk move does (Checker.besideNow).
out = append(out, BesideRef{Repository: repo, Commit: commit, atTrunk: atTrunk, runs: !atTrunk})
}
default:
if member := c.member(repo); member != nil {
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)})
} else if head := heads[repo]; head != "" {
out = append(out, BesideRef{Repository: repo, Commit: head, atTrunk: true})
}
}
}
for _, repo := range besideRules[p.Repository] {
place(repo)
}
// A sibling a test builds needs every repository its go.mod requires at the zero version beside it too: the build
// resolves them from what lies beside, with nothing fetched (issue 1921). Its go.mod names them all, indirect ones
// included, so one level is enough.
derived, err := j.zeroVersionSiblings(ctx, p.Repository, out)
if err != nil {
return nil, err
}
for _, repo := range derived {
place(repo)
}
return out, nil
}
// zeroVersionSiblings are the repositories the root go.mod of each sibling in refs requires at the zero version that are
// not beside already, sorted; none where the checker reads no forge. A sibling with no root go.mod requires none; one the
// forge does not answer for is an error, never a sibling left out in silence.
func (j *Examiner) zeroVersionSiblings(ctx context.Context, own string, refs []BesideRef) ([]string, error) {
if j.Forge == nil {
return nil, nil
}
have := map[string]bool{own: true}
for _, r := range refs {
have[r.Repository] = true
}
var out []string
for _, r := range refs {
if r.Repository == own {
continue
}
text, err := j.Forge.File(ctx, r.Repository, "go.mod", r.Commit)
if errors.Is(err, ErrNotFound) {
continue
}
if err != nil {
return nil, fmt.Errorf("the go.mod of %s at %.12s, beside the change, cannot be read%s: %w", r.Repository, r.Commit, unheardWords(err), err)
}
for _, path := range zeroVersionRequires(text) {
repo, _, ok := trunk.PackageRepository(path)
if !ok || have[repo] {
continue
}
have[repo] = true
out = append(out, repo)
}
}
slices.Sort(out)
return out, nil
}
// zeroVersionRequires are the module paths a go.mod's require lines (a block or single ones) name at the zero version.
func zeroVersionRequires(gomod string) []string {
var out []string
block := false
for _, line := range strings.Split(gomod, "\n") {
line, _, _ = strings.Cut(line, "//")
f := strings.Fields(line)
switch {
case len(f) == 0:
continue
case f[0] == "require" && len(f) > 1 && f[1] == "(":
block = true
continue
case block && f[0] == ")":
block = false
continue
case f[0] == "require":
f = f[1:]
case !block:
continue
}
if len(f) >= 2 && f[1] == zeroVersion {
out = append(out, f[0])
}
}
return out
}
// zeroVersion is the version a go.mod requires a mesh package at (ADR 0573 §4): the builder writes the version it builds.
const zeroVersion = "v0.0.0-00010101000000-000000000000"
func (c *Case) member(repository string) *Pull {
for i := range c.Pulls {
if c.Pulls[i].Repository == repository {
return &c.Pulls[i]
}
}
return nil
}
func refOf(number int) string {
if number <= 0 {
return ""
}
return "refs/pull/" + strconv.Itoa(number) + "/head"
}
// besideAtTrunk are the siblings of refs read at their trunk's head that the case does not merge, by repository: what a
// verdict names as Beside (ADR 0548 §6).
func besideAtTrunk(c *Case, refs []BesideRef, into map[string]string) {
for _, r := range refs {
if _, merged := c.Trunk[r.Repository]; (r.atTrunk || r.runs) && !merged {
into[r.Repository] = r.Commit
}
}
}
// NodeEngineRepository is the node-engine's repository, whose trunk head a check without its member composes at.
const NodeEngineRepository = "novox/mesh-host"