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
152 lines
6.1 KiB
Go
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
|
|
}
|