Read seat definitions once: the indexer's reading becomes a module the checker requires (issue 1921)

The checking program's seat list parsed the definition format a second time beside the
indexer's judge. The indexer's reading moves whole into its own sibling module seatfiles
(a domain path, as takein), with ResolveRaw keeping every field as written for a request's
seats; the judge and the take-in read through it, and the checker's rules only find the
definitions and say why one is left out (the lead's ruling on D3, ADR 0634 §1).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 22:39:31 +01:00
parent 35da943fb5
commit 043e3fb979
10 changed files with 228 additions and 188 deletions
+4
View File
@@ -29,3 +29,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
require git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles v0.0.0-00010101000000-000000000000
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles => ../mesh-delivery-indexer/seatfiles
+17 -51
View File
@@ -8,11 +8,15 @@ import (
"path"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles"
)
// The seat list a check makes from its heads (novox/hq ADR 0636 §4, ADR 0644 §1; the lead's ruling Q2 on milestone
// 47/B0): the checking program built from the heads reads the seat definitions they carry and answers them resolved, so
// the running checker hands the manifest validator and the composer bytes it does not read itself.
// the running checker hands the manifest validator and the composer bytes it does not read itself. A definition is read
// by the indexer's one reading of the format (package seatfiles); this file finds the definitions and says why one is
// left out.
// SeatsVersion is the version of SeatsAnswer this program writes.
const SeatsVersion = 1
@@ -60,14 +64,12 @@ func SeatsOf(heads fs.FS) (SeatsAnswer, error) {
}
bySeat := map[string][]found{}
for _, p := range files {
raw, err := resolvedDefinition(heads, p)
seat, raw, err := resolvedDefinition(heads, p)
if err != nil {
out.Left = append(out.Left, LeftOut{Path: p, Why: err.Error()})
continue
}
var d definitionHead
_ = json.Unmarshal(raw, &d)
bySeat[d.Seat] = append(bySeat[d.Seat], found{p, raw})
bySeat[seat] = append(bySeat[seat], found{p, raw})
}
for _, seat := range slices.Sorted(maps.Keys(bySeat)) {
fs := bySeat[seat]
@@ -86,57 +88,21 @@ func SeatsOf(heads fs.FS) (SeatsAnswer, error) {
return out, nil
}
// definitionHead is what of a definition the list is made by: its seat, its scope and the parts it includes. The rest is
// carried as written, for the manifest validator and the composer to read (milestone G gives this the domain SDK's one
// reading).
type definitionHead struct {
Seat string `json:"seat"`
Scope string `json:"scope"`
Includes []string `json:"includes"`
}
// resolvedDefinition is the definition at p with its parts' verbs and events put first and no includes, every field it
// carries kept as written.
func resolvedDefinition(heads fs.FS, p string) (json.RawMessage, error) {
// resolvedDefinition is the definition at p resolved with the parts it includes from the `parts` directory beside its
// `seats` directory, as the indexer's one reading of the format resolves it (seatfiles.ResolveRaw), every field kept as
// written.
func resolvedDefinition(heads fs.FS, p string) (string, json.RawMessage, error) {
raw, err := fs.ReadFile(heads, p)
if err != nil {
return nil, err
return "", nil, err
}
var d definitionHead
if err := json.Unmarshal(raw, &d); err != nil {
return nil, fmt.Errorf("a seat definition is not JSON: %w", err)
}
if d.Seat == "" || (d.Scope != "mesh" && d.Scope != "node") {
return nil, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope)
}
var whole map[string]json.RawMessage
if err := json.Unmarshal(raw, &whole); err != nil {
return nil, err
}
var verbs, events []json.RawMessage
partsDir := path.Join(path.Dir(path.Dir(path.Dir(p))), "parts")
for _, name := range d.Includes {
d, out, err := seatfiles.ResolveRaw(raw, func(name string) ([]byte, error) {
praw, err := fs.ReadFile(heads, path.Join(partsDir, name+".json"))
if err != nil {
return nil, fmt.Errorf("the seat %s includes the part %q, which is not at %s", d.Seat, name, partsDir)
return nil, fmt.Errorf("it is not at %s", partsDir)
}
var part struct{ Verbs, Events []json.RawMessage }
if err := json.Unmarshal(praw, &part); err != nil {
return nil, fmt.Errorf("the part %q of the seat %s is not JSON: %w", name, d.Seat, err)
}
verbs, events = append(verbs, part.Verbs...), append(events, part.Events...)
}
var own struct{ Verbs, Events []json.RawMessage }
_ = json.Unmarshal(raw, &own)
verbs, events = append(verbs, own.Verbs...), append(events, own.Events...)
delete(whole, "includes")
for k, list := range map[string][]json.RawMessage{"verbs": verbs, "events": events} {
if len(list) == 0 {
delete(whole, k)
continue
}
b, _ := json.Marshal(list)
whole[k] = b
}
return json.Marshal(whole)
return praw, nil
})
return d.Seat, out, err
}
+3
View File
@@ -26,6 +26,7 @@ replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
)
@@ -34,4 +35,6 @@ replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ./takein
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles => ./seatfiles
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ../mesh-delivery-forge-bridge/forgemsg
@@ -18,9 +18,8 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-delivery-indexer/judge"
)
// definitionsDir is the directory of a contracts module's definitions that the module.json at dir names, from the first
@@ -58,7 +57,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c
if ix.ReadFile == nil || ix.Files == nil {
return nil, nil
}
var defs []judge.Definition
var defs []seatfiles.Definition
for _, e := range ready {
if e.Covered || e.Forge.Merge == "" {
continue
@@ -75,7 +74,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c
defs = append(defs, got...)
}
}
seats, err := judge.Seats(defs)
seats, err := seatfiles.Seats(defs)
if err != nil {
return nil, err
}
@@ -84,7 +83,7 @@ func (ix *Indexer) revisionSeats(ctx context.Context, ready []PendingEntry) ([]c
// readDefinitions reads the definitions under a contracts module's definitions directory at a commit, resolved with their
// parts.
func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defsDir string) ([]judge.Definition, error) {
func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defsDir string) ([]seatfiles.Definition, error) {
file := func(p string) ([]byte, error) {
if defsDir != "." && defsDir != "" {
p = defsDir + "/" + p
@@ -103,8 +102,8 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs
if err != nil {
return nil, fmt.Errorf("listing %s of %s at %.12s: %w", dir, repository, commit, err)
}
parts := map[string]judge.Part{}
var out []judge.Definition
parts := map[string]seatfiles.Part{}
var out []seatfiles.Definition
for _, p := range slices.Sorted(maps.Keys(listed[dir])) {
seat, ok := strings.CutSuffix(p, "/seat.json")
if !ok || seat == "" || strings.Contains(seat, "/") {
@@ -114,7 +113,7 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs
if err != nil {
return nil, err
}
d, err := judge.ParseDefinition(raw)
d, err := seatfiles.ParseDefinition(raw)
if err == nil && d.Seat != seat {
err = fmt.Errorf("it names the seat %q, not %q", d.Seat, seat)
}
@@ -129,11 +128,11 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit, defs
if perr != nil {
return nil, perr
}
var part judge.Part
var part seatfiles.Part
if praw == nil {
perr = errors.New("the commit holds no such part")
} else {
part, perr = judge.ParsePart(praw)
part, perr = seatfiles.ParsePart(praw)
}
if perr != nil {
err = fmt.Errorf("its part %s: %w", name, perr)
+2 -1
View File
@@ -25,6 +25,7 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
)
@@ -212,7 +213,7 @@ func Request(r composer.Request) (composer.Verdict, error) { return RequestWith(
// the members' heads' definitions, so a manifest that serves or invokes what the group's definitions add is judged by
// them. None is Request.
func RequestWith(r composer.Request, seats [][]byte) (composer.Verdict, error) {
over, err := SeatsOfFiles(seats)
over, err := seatfiles.SeatsOfFiles(seats)
if err != nil {
return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err)
}
@@ -6,6 +6,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/overlay"
@@ -155,8 +156,8 @@ func TestTheJudge(t *testing.T) {
t.Fatalf("%s: the seats are not put back", row.name)
}
}
parts := map[string]Part{"reads": {Verbs: []DefVerb{{Name: "manifest-reads"}}}}
d, _ := ParseDefinition(defs[0])
parts := map[string]seatfiles.Part{"reads": {Verbs: []seatfiles.DefVerb{{Name: "manifest-reads"}}}}
d, _ := seatfiles.ParseDefinition(defs[0])
if d, err := d.Resolve(parts); err != nil || len(d.Verbs) != 2 || d.Verbs[0].Name != "manifest-reads" {
t.Errorf("a resolved definition: %+v %v", d, err)
}
@@ -1,137 +1,15 @@
package judge
// The seat definitions a revision carries (novox/hq ADR 0633 §1, §6; ADR 0635, ADR 0636 §4): a manifest is judged
// against the seats of the revision it belongs to, never against the list this program was built with. A definition is
// the contracts module's `seats/<seat>/seat.json` (design 26 *A seat's definition*), with the parts it includes from
// `parts/<part>.json`. This reader takes of it only what a manifest is judged by (the seat's scope, what it delivers,
// its verbs and events) and reads the rest leniently: the format's one strict reader (mesh-sdk seatdef) is not in the
// mesh-sdk this judge is built with, and the indexer of the revision keeps the definitions whole.
// The seat list a judge pass judges with: the seats a request or a revision carries (read by the indexer's one reading,
// package seatfiles) put over the catalogue's list for that pass.
import (
"encoding/json"
"fmt"
"slices"
"sync"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Definition is the part of one seat's definition a manifest is judged by.
type Definition struct {
Format int `json:"format"`
Seat string `json:"seat"`
Scope string `json:"scope"`
Decision string `json:"decision"`
HolderDeclares bool `json:"holder-declares"`
Delivers string `json:"delivers"`
Replicated bool `json:"replicated"`
PulledByKind bool `json:"pulled-by-kind"`
Yields bool `json:"yields"`
HeldByTheEngine bool `json:"held-by-the-engine"`
Includes []string `json:"includes"`
Verbs []DefVerb `json:"verbs"`
Accepts []DefName `json:"accepts"`
Events []DefName `json:"events"`
}
// DefVerb is a verb of a definition.
type DefVerb struct {
Name string `json:"name"`
Description string `json:"description"`
Optional bool `json:"optional"`
Replaces []string `json:"replaces"`
}
// DefName is an accepted message or an event of a definition.
type DefName struct {
Name string `json:"name"`
}
// Part is a part a definition includes: verbs and events written once for the seats that share them.
type Part struct {
Part string `json:"part"`
Verbs []DefVerb `json:"verbs"`
Events []DefName `json:"events"`
}
// ParseDefinition reads one definition; a file that is not one names a seat and a scope.
func ParseDefinition(raw []byte) (Definition, error) {
var d Definition
if err := json.Unmarshal(raw, &d); err != nil {
return Definition{}, fmt.Errorf("a seat definition is not JSON: %w", err)
}
if d.Seat == "" || (d.Scope != catalogue.ScopeMesh && d.Scope != catalogue.ScopeNode) {
return Definition{}, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope)
}
return d, nil
}
// ParsePart reads one part.
func ParsePart(raw []byte) (Part, error) {
var p Part
if err := json.Unmarshal(raw, &p); err != nil {
return Part{}, fmt.Errorf("a part of a seat definition is not JSON: %w", err)
}
return p, nil
}
// Resolve is the definition with the verbs and events of the parts it includes put first, as the indexer keeps it.
func (d Definition) Resolve(parts map[string]Part) (Definition, error) {
out := d
out.Includes, out.Verbs, out.Events = nil, nil, nil
for _, name := range d.Includes {
p, ok := parts[name]
if !ok {
return Definition{}, fmt.Errorf("the seat %s includes the part %q, which is not given", d.Seat, name)
}
out.Verbs = append(out.Verbs, p.Verbs...)
out.Events = append(out.Events, p.Events...)
}
out.Verbs = append(out.Verbs, d.Verbs...)
out.Events = append(out.Events, d.Events...)
return out, nil
}
// Seats are the seats of resolved definitions, in the catalogue's types; a seat its holder declares itself is left out.
// A definition still naming parts it includes is refused: resolve it first.
func Seats(defs []Definition) ([]catalogue.Seat, error) {
var out []catalogue.Seat
for _, d := range defs {
if d.HolderDeclares {
continue
}
if len(d.Includes) > 0 {
return nil, fmt.Errorf("the seat %s names parts it includes: resolve it first", d.Seat)
}
s := catalogue.Seat{Name: d.Seat, Scope: d.Scope, Delivers: d.Delivers, Replicated: d.Replicated,
PulledByKind: d.PulledByKind, Yields: d.Yields, HeldByTheEngine: d.HeldByTheEngine, Decision: d.Decision}
for _, a := range d.Accepts {
s.Accepts = append(s.Accepts, a.Name)
}
for _, e := range d.Events {
s.Emits = append(s.Emits, e.Name)
}
for _, v := range d.Verbs {
s.Serves = append(s.Serves, catalogue.Verb{Name: v.Name, Description: v.Description, Optional: v.Optional, Replaces: v.Replaces})
}
out = append(out, s)
}
return out, nil
}
// SeatsOfFiles are the seats of resolved definitions given as files, as a request carries them.
func SeatsOfFiles(raws [][]byte) ([]catalogue.Seat, error) {
defs := make([]Definition, 0, len(raws))
for _, raw := range raws {
d, err := ParseDefinition(raw)
if err != nil {
return nil, err
}
defs = append(defs, d)
}
return Seats(defs)
}
// merged is base with each seat of over put in place of the seat of its name, or added: a definition is a seat's whole
// contract. A verb the base seat holds keeps its input and output (a definition read here carries none).
func merged(base, over []catalogue.Seat) []catalogue.Seat {
@@ -0,0 +1,5 @@
module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/seatfiles
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
@@ -0,0 +1,181 @@
// Package seatfiles is the indexer's one reading of the seat definitions a revision carries (novox/hq ADR 0633 §1, §6;
// ADR 0635, ADR 0636 §4): a manifest is judged against the seats of the revision it belongs to, never against the list
// the judge was built with. A definition is the contracts module's `seats/<seat>/seat.json` (design 26 *A seat's
// definition*), with the parts it includes from `parts/<part>.json`. This reader takes of it only what a manifest is
// judged by (the seat's scope, what it delivers, its verbs and events) and reads the rest leniently: the format's one
// strict reader (mesh-sdk seatdef) is not in the mesh-sdk the judge is built with, and the indexer of the revision keeps
// the definitions whole.
//
// It is the indexer's own package, a module of its own so that the checker's rules can require it (the lead's ruling on
// milestone 47/B0, ADR 0634 §1: shared code lives with its owner): the indexer's judge and the checker's seat list read
// definitions here and nowhere else. Milestone G's domain SDK reading replaces it.
package seatfiles
import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Definition is the part of one seat's definition a manifest is judged by.
type Definition struct {
Format int `json:"format"`
Seat string `json:"seat"`
Scope string `json:"scope"`
Decision string `json:"decision"`
HolderDeclares bool `json:"holder-declares"`
Delivers string `json:"delivers"`
Replicated bool `json:"replicated"`
PulledByKind bool `json:"pulled-by-kind"`
Yields bool `json:"yields"`
HeldByTheEngine bool `json:"held-by-the-engine"`
Includes []string `json:"includes"`
Verbs []DefVerb `json:"verbs"`
Accepts []DefName `json:"accepts"`
Events []DefName `json:"events"`
}
// DefVerb is a verb of a definition.
type DefVerb struct {
Name string `json:"name"`
Description string `json:"description"`
Optional bool `json:"optional"`
Replaces []string `json:"replaces"`
}
// DefName is an accepted message or an event of a definition.
type DefName struct {
Name string `json:"name"`
}
// Part is a part a definition includes: verbs and events written once for the seats that share them.
type Part struct {
Part string `json:"part"`
Verbs []DefVerb `json:"verbs"`
Events []DefName `json:"events"`
}
// ParseDefinition reads one definition; a file that is not one names a seat and a scope.
func ParseDefinition(raw []byte) (Definition, error) {
var d Definition
if err := json.Unmarshal(raw, &d); err != nil {
return Definition{}, fmt.Errorf("a seat definition is not JSON: %w", err)
}
if d.Seat == "" || (d.Scope != catalogue.ScopeMesh && d.Scope != catalogue.ScopeNode) {
return Definition{}, fmt.Errorf("a seat definition names its seat and its scope (mesh or node), not %q and %q", d.Seat, d.Scope)
}
return d, nil
}
// ParsePart reads one part.
func ParsePart(raw []byte) (Part, error) {
var p Part
if err := json.Unmarshal(raw, &p); err != nil {
return Part{}, fmt.Errorf("a part of a seat definition is not JSON: %w", err)
}
return p, nil
}
// Resolve is the definition with the verbs and events of the parts it includes put first, as the indexer keeps it.
func (d Definition) Resolve(parts map[string]Part) (Definition, error) {
out := d
out.Includes, out.Verbs, out.Events = nil, nil, nil
for _, name := range d.Includes {
p, ok := parts[name]
if !ok {
return Definition{}, fmt.Errorf("the seat %s includes the part %q, which is not given", d.Seat, name)
}
out.Verbs = append(out.Verbs, p.Verbs...)
out.Events = append(out.Events, p.Events...)
}
out.Verbs = append(out.Verbs, d.Verbs...)
out.Events = append(out.Events, d.Events...)
return out, nil
}
// Seats are the seats of resolved definitions, in the catalogue's types; a seat its holder declares itself is left out.
// A definition still naming parts it includes is refused: resolve it first.
func Seats(defs []Definition) ([]catalogue.Seat, error) {
var out []catalogue.Seat
for _, d := range defs {
if d.HolderDeclares {
continue
}
if len(d.Includes) > 0 {
return nil, fmt.Errorf("the seat %s names parts it includes: resolve it first", d.Seat)
}
s := catalogue.Seat{Name: d.Seat, Scope: d.Scope, Delivers: d.Delivers, Replicated: d.Replicated,
PulledByKind: d.PulledByKind, Yields: d.Yields, HeldByTheEngine: d.HeldByTheEngine, Decision: d.Decision}
for _, a := range d.Accepts {
s.Accepts = append(s.Accepts, a.Name)
}
for _, e := range d.Events {
s.Emits = append(s.Emits, e.Name)
}
for _, v := range d.Verbs {
s.Serves = append(s.Serves, catalogue.Verb{Name: v.Name, Description: v.Description, Optional: v.Optional, Replaces: v.Replaces})
}
out = append(out, s)
}
return out, nil
}
// SeatsOfFiles are the seats of resolved definitions given as files, as a request carries them.
func SeatsOfFiles(raws [][]byte) ([]catalogue.Seat, error) {
defs := make([]Definition, 0, len(raws))
for _, raw := range raws {
d, err := ParseDefinition(raw)
if err != nil {
return nil, err
}
defs = append(defs, d)
}
return Seats(defs)
}
// ResolveRaw is the definition raw resolved with the parts it includes, each read by part, every field it carries kept as
// written: the verbs and events of its parts put first, then its own, and no includes. It reads raw and every part as
// ParseDefinition and ParsePart do, and answers the definition read with it. It is how a request's `seats` carry a
// definition whole (ADR 0636 §4).
func ResolveRaw(raw []byte, part func(name string) ([]byte, error)) (Definition, json.RawMessage, error) {
d, err := ParseDefinition(raw)
if err != nil {
return Definition{}, nil, err
}
var whole map[string]json.RawMessage
if err := json.Unmarshal(raw, &whole); err != nil {
return Definition{}, nil, err
}
type listed struct{ Verbs, Events []json.RawMessage }
var verbs, events []json.RawMessage
for _, name := range d.Includes {
praw, err := part(name)
if err != nil {
return Definition{}, nil, fmt.Errorf("the seat %s includes the part %q: %w", d.Seat, name, err)
}
var l listed
if _, err := ParsePart(praw); err != nil {
return Definition{}, nil, fmt.Errorf("the part %q of the seat %s: %w", name, d.Seat, err)
}
_ = json.Unmarshal(praw, &l)
verbs, events = append(verbs, l.Verbs...), append(events, l.Events...)
}
var own listed
_ = json.Unmarshal(raw, &own)
verbs, events = append(verbs, own.Verbs...), append(events, own.Events...)
delete(whole, "includes")
for k, list := range map[string][]json.RawMessage{"verbs": verbs, "events": events} {
if len(list) == 0 {
delete(whole, k)
continue
}
b, err := json.Marshal(list)
if err != nil {
return Definition{}, nil, err
}
whole[k] = b
}
out, err := json.Marshal(whole)
return d, out, err
}