Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/headseats.go
T
jschoubben 67127ce60d The checker takes a head's seat definitions as the indexer does, from one reading in the domain SDK, so an outside definer is never laid over the seat list (ADR 0636 §5, ADR 0634 §1-2; issue 1966)
The checker read any touched contracts module's definitions, skipped the
check that a definition names its own directory's seat, and copied the
indexer's tree reading. sdk/seatdefs now holds the reading and the rule of
which contracts module may define a seat; the indexer's keeping, its
revision seats and the checker's head seats all use it. Two heads defining
one seat fail the judging unless one hands it over to its domain's module.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 10:37:00 +01:00

152 lines
6.1 KiB
Go

package check
// The seat definitions of the members' heads (novox/hq ADR 0636 §4, ADR 0633 §6 as its insight of 2026-11-05 (2) says): the
// manifest validator inside a check is the one reader that reads them. A manifest of a group may serve or invoke what the
// group's contracts modules add to a seat (a verb, an event), which the seat list the checker follows (the trunks') does not
// hold yet; judged without them, by the trunk's manifest validator above all, the group's new indexer would be refused for
// serving its own seat's new verb, and so would every module that calls it. The definitions are read from each member's head
// with the indexer's own reading (the domain SDK's seatdefs: the contracts module's `seats/<seat>/seat.json` with the parts it
// includes, and the rule which module may define a seat), through the forge, and put over the list's in the request's Seats.
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"git.novox.be/novox/mesh-delivery/sdk/seatdefs"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Trees lists the files of a repository under directories at a commit (the forge's commit trees): {directory: {path
// relative to it: blob id}}. A directory the commit lacks is an error wrapping ErrNotFound.
type Trees interface {
Files(ctx context.Context, repository, commit string, dirs []string) (map[string]map[string]string, error)
}
// headSeats are the definitions, resolved, that the contracts modules the case's members touch hold at their heads, by
// seat; none where the checker lists no trees. They are read and ruled on as the indexer reads and rules on a revision's,
// with the domain SDK's one reading (mesh-delivery sdk/seatdefs): a seat of the mesh's own contracts is taken only from
// its domain's contracts module (ADR 0636 §5), so an outside definer's is left out and said, and a seat two touched modules
// define is refused unless one hands it over to its domain's module. A head's definition that cannot be read, does not
// parse or names another seat than its directory's is an error: the manifests cannot be judged by the group's seats then.
func (st *state) headSeats(ctx context.Context) (map[string]json.RawMessage, error) {
if st.j.Trees == nil || st.j.Forge == nil {
return nil, nil
}
out := map[string]json.RawMessage{}
by := map[string]string{} // seat -> the module whose head's definition is taken
for _, p := range st.c.Pulls {
for _, module := range p.Modules {
c, ok, err := st.contractsModule(ctx, p, module)
if err != nil {
return nil, err
}
if !ok {
continue
}
defs, unparsed, err := st.headForge().Read(ctx, p.Repository, p.Head, c)
if err != nil {
return nil, fmt.Errorf("the seat definitions of %s at the head of %s cannot be read%s: %w", module, pullKey(p), unheardWords(err), err)
}
if len(unparsed) > 0 {
seat := slices.Sorted(maps.Keys(unparsed))[0]
return nil, fmt.Errorf("the seat definition of %s (module %s) at the head of %s is not taken: %s", seat, module, pullKey(p), unparsed[seat])
}
for _, seat := range slices.Sorted(maps.Keys(defs)) {
d := defs[seat]
if why := seatdefs.Refusal(c.Module, d); why != "" {
st.c.note(fmt.Sprintf("the head of %s's definition of %s is not taken: %s", pullKey(p), seat, why))
continue
}
if prev, twice := by[seat]; twice && prev != c.Module {
switch {
case seatdefs.HandsOver(prev, c.Module, d):
case seatdefs.HandsOver(c.Module, prev, d):
continue
default:
return nil, fmt.Errorf("the seat %s is defined at the heads by both %s and %s: a seat has one definition", seat, prev, c.Module)
}
}
raw, err := json.Marshal(d)
if err != nil {
return nil, fmt.Errorf("the definition of %s cannot be encoded: %w", seat, err)
}
out[seat], by[seat] = raw, c.Module
}
}
}
return out, nil
}
// headForge is the domain SDK's reading bound to the checker's forge listing and get-file: a directory or a file the head
// does not hold is none, every other failure the forge's.
func (st *state) headForge() seatdefs.Forge {
return seatdefs.Forge{
Files: func(ctx context.Context, repository, commit string, dirs []string) (map[string]map[string]string, error) {
got, err := st.j.Trees.Files(ctx, repository, commit, dirs)
if errors.Is(err, ErrNotFound) {
return nil, nil
}
return got, err
},
File: func(ctx context.Context, repository, commit, file string) ([]byte, error) {
text, err := st.j.Forge.File(ctx, repository, file, commit)
if errors.Is(err, ErrNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return []byte(text), nil
},
}
}
// contractsModule is a touched module as a contracts module at its member's head (its package artifact's generated-from,
// under the module's directory), and whether the module is a contracts module at all.
func (st *state) contractsModule(ctx context.Context, p Pull, module string) (seatdefs.Module, bool, error) {
var m catalogue.Manifest
dir, known := "", false
if st.j.Dirs != nil {
dir, known = st.j.Dirs.ModuleDir(module)
}
if digest := p.Fps[module].Manifest; known && digest != "" && st.j.Read != nil {
var err error
if m, err = st.j.Read.Manifest(ctx, digest); err != nil {
return seatdefs.Module{}, false, fmt.Errorf("the manifest of %s at the head of %s cannot be read: %w", module, pullKey(p), err)
}
} else {
var found bool
var err error
if m, dir, found, err = st.manifestAtHead(ctx, p, module); err != nil || !found {
return seatdefs.Module{}, false, err
}
}
c, ok := seatdefs.Of(dir, m)
return c, ok, nil
}
// overSeats is the list's definitions with each of the heads' put in place of the one of its seat, or added, sorted by seat.
func overSeats(list []json.RawMessage, heads map[string]json.RawMessage) []json.RawMessage {
if len(heads) == 0 {
return list
}
by := map[string]json.RawMessage{}
for _, raw := range list {
var d struct {
Seat string `json:"seat"`
}
_ = json.Unmarshal(raw, &d)
by[d.Seat] = raw
}
maps.Copy(by, heads)
out := make([]json.RawMessage, 0, len(by))
for _, seat := range slices.Sorted(maps.Keys(by)) {
out = append(out, by[seat])
}
return out
}