The checking program answers the seat list of the heads laid for it (issue 1921)
Asked -seats, it reads every seat definition the heads carry, resolves the parts it includes and answers the list with what it left out and why, so the running checker hands the manifest validator and the composer a list it does not read itself (ADR 0636 §4, ADR 0644 §1). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -28,6 +28,7 @@ func run(args []string, getenv func(string) string, stdout, stderr io.Writer) in
|
||||
fs := flag.NewFlagSet("mesh-checking-program", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
verdict := fs.Bool("verdict", false, "answer the verdict of the facts in the input")
|
||||
seats := fs.Bool("seats", false, "answer the seat list of the heads laid for it")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
@@ -35,8 +36,24 @@ func run(args []string, getenv func(string) string, stdout, stderr io.Writer) in
|
||||
fmt.Fprintf(stderr, "mesh-checking-program: "+format+"\n", a...)
|
||||
return 2
|
||||
}
|
||||
if *seats {
|
||||
dir := getenv(delivery.RunHeadsEnv)
|
||||
if dir == "" {
|
||||
return refuse("no heads: %s names no directory", delivery.RunHeadsEnv)
|
||||
}
|
||||
answer, err := rules.SeatsOf(os.DirFS(dir))
|
||||
if err != nil {
|
||||
return refuse("%v", err)
|
||||
}
|
||||
line, err := json.Marshal(answer)
|
||||
if err != nil {
|
||||
return refuse("the seat list cannot be written: %v", err)
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\n", line)
|
||||
return 0
|
||||
}
|
||||
if !*verdict {
|
||||
return refuse("nothing asked: give -verdict")
|
||||
return refuse("nothing asked: give -verdict or -seats")
|
||||
}
|
||||
file := getenv(delivery.RunInputEnv)
|
||||
if file == "" {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
|
||||
// 1921 (B0, D2): the checking program answers, on its last line, the verdict the rules give the facts in its input, and
|
||||
// refuses facts of a version it does not read, an input it is not given, and a question it is not asked, naming each.
|
||||
// 1921 (B0, D3): asked -seats, it answers the seat list of the heads laid for it: a definition a head adds, resolved with
|
||||
// the parts it includes; what it left out, and why; and an empty list for heads with no definition.
|
||||
func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) {
|
||||
failing, _ := json.Marshal(rules.Tested{Level: delivery.LevelUnit, Tests: 3, Failed: []rules.Failure{{Package: "x/a", Test: "TestA", Cause: "a_test.go:9: boom"}}})
|
||||
facts := rules.Facts{V: rules.FactsVersion, Group: "feat/x", Heads: "0123456789ab", Members: []rules.Member{{Repository: "novox/x", Number: 3, Head: "h"}},
|
||||
@@ -35,22 +37,45 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) {
|
||||
later := facts
|
||||
later.V = rules.FactsVersion + 1
|
||||
v2 := write("v2.json", later)
|
||||
heads := filepath.Join(dir, "heads")
|
||||
for p, body := range map[string]string{
|
||||
"novox/mesh-contracts/contracts/seats/node-build-x/seat.json": `{"format":1,"seat":"node-build-x","scope":"node","description":"kept","includes":["build"],"verbs":[{"name":"own"}]}`,
|
||||
"novox/mesh-contracts/contracts/parts/build.json": `{"part":"build","verbs":[{"name":"current"}],"events":[{"name":"ended"}]}`,
|
||||
"novox/mesh-contracts/contracts/seats/broken/seat.json": `{"format":1,"seat":"broken","scope":"nowhere"}`,
|
||||
} {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(heads, p)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(heads, p), []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
none := filepath.Join(dir, "no-heads")
|
||||
if err := os.MkdirAll(filepath.Join(none, "novox/x"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
args []string
|
||||
input string
|
||||
exit int
|
||||
said string
|
||||
args []string
|
||||
input, heads string
|
||||
exit int
|
||||
said string
|
||||
}{
|
||||
"the facts": {[]string{"-verdict"}, v1, 0, ""},
|
||||
"a later version": {[]string{"-verdict"}, v2, 2, "facts of version 2, this checking program reads version 1"},
|
||||
"no input": {[]string{"-verdict"}, "", 2, delivery.RunInputEnv + " names no file"},
|
||||
"nothing asked": {nil, v1, 2, "nothing asked"},
|
||||
"an unknown asking": {[]string{"-judge"}, v1, 2, "-judge"},
|
||||
"the seats": {args: []string{"-seats"}, heads: heads},
|
||||
"no seats": {args: []string{"-seats"}, heads: none},
|
||||
"no heads laid": {args: []string{"-seats"}, exit: 2, said: delivery.RunHeadsEnv + " names no directory"},
|
||||
"the facts": {args: []string{"-verdict"}, input: v1},
|
||||
"a later version": {args: []string{"-verdict"}, input: v2, exit: 2, said: "facts of version 2, this checking program reads version 1"},
|
||||
"no input": {args: []string{"-verdict"}, exit: 2, said: delivery.RunInputEnv + " names no file"},
|
||||
"nothing asked": {input: v1, exit: 2, said: "nothing asked"},
|
||||
"an unknown asking": {args: []string{"-judge"}, input: v1, exit: 2, said: "-judge"},
|
||||
} {
|
||||
var out, errs bytes.Buffer
|
||||
exit := run(c.args, func(k string) string {
|
||||
if k == delivery.RunInputEnv {
|
||||
switch k {
|
||||
case delivery.RunInputEnv:
|
||||
return c.input
|
||||
case delivery.RunHeadsEnv:
|
||||
return c.heads
|
||||
}
|
||||
return ""
|
||||
}, &out, &errs)
|
||||
@@ -62,6 +87,23 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
if c.heads != "" {
|
||||
var sa rules.SeatsAnswer
|
||||
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &sa); err != nil || sa.V != rules.SeatsVersion || sa.Seats == nil {
|
||||
t.Fatalf("%s: the last line %q: %v", name, lines[len(lines)-1], err)
|
||||
}
|
||||
if c.heads == none {
|
||||
if len(sa.Seats) != 0 || len(sa.Left) != 0 {
|
||||
t.Errorf("%s: %+v, want an empty list", name, sa)
|
||||
}
|
||||
continue
|
||||
}
|
||||
want := `{"description":"kept","events":[{"name":"ended"}],"format":1,"scope":"node","seat":"node-build-x","verbs":[{"name":"current"},{"name":"own"}]}`
|
||||
if len(sa.Seats) != 1 || string(sa.Seats[0]) != want || len(sa.Left) != 1 || !strings.Contains(sa.Left[0].Why, `not "broken" and "nowhere"`) {
|
||||
t.Errorf("%s: seats %s, left %+v; want %s and broken left out", name, sa.Seats, sa.Left, want)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var a rules.Answer
|
||||
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &a); err != nil || a.V != rules.AnswerVersion {
|
||||
t.Fatalf("%s: the last line %q: %v", name, lines[len(lines)-1], err)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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.
|
||||
|
||||
// SeatsVersion is the version of SeatsAnswer this program writes.
|
||||
const SeatsVersion = 1
|
||||
|
||||
// SeatsAnswer is the checking program's `-seats` answer: every seat definition the heads carry, resolved (the verbs and
|
||||
// events of the parts it includes put first, as the indexer keeps it), as the requests' field `seats` carries them; and
|
||||
// each definition left out, with why.
|
||||
type SeatsAnswer struct {
|
||||
V int `json:"v"`
|
||||
Seats []json.RawMessage `json:"seats"`
|
||||
Left []LeftOut `json:"left,omitempty"`
|
||||
}
|
||||
|
||||
// LeftOut is a seat definition the list does not hold: its file and why.
|
||||
type LeftOut struct {
|
||||
Path string `json:"path"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// SeatsOf reads the seat definitions in heads, a directory holding each head at its repository's path: a definition is a
|
||||
// contracts module's `seats/<seat>/seat.json`, with the parts it includes from the `parts/<part>.json` beside that
|
||||
// `seats` directory (design 26, *A seat's definition*). A file that is not a definition, a part it names that is not
|
||||
// there, and a seat defined in more than one place are left out, said; heads with no definition give an empty list.
|
||||
func SeatsOf(heads fs.FS) (SeatsAnswer, error) {
|
||||
out := SeatsAnswer{V: SeatsVersion, Seats: []json.RawMessage{}}
|
||||
var files []string
|
||||
err := fs.WalkDir(heads, ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() && (d.Name() == ".git" || d.Name() == "testdata") {
|
||||
return fs.SkipDir
|
||||
}
|
||||
if !d.IsDir() && d.Name() == "seat.json" && path.Base(path.Dir(path.Dir(p))) == "seats" {
|
||||
files = append(files, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SeatsAnswer{}, fmt.Errorf("rules: the heads cannot be read: %w", err)
|
||||
}
|
||||
type found struct {
|
||||
path string
|
||||
raw json.RawMessage
|
||||
}
|
||||
bySeat := map[string][]found{}
|
||||
for _, p := range files {
|
||||
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})
|
||||
}
|
||||
for _, seat := range slices.Sorted(maps.Keys(bySeat)) {
|
||||
fs := bySeat[seat]
|
||||
if len(fs) > 1 {
|
||||
var paths []string
|
||||
for _, f := range fs {
|
||||
paths = append(paths, f.path)
|
||||
}
|
||||
for _, f := range fs {
|
||||
out.Left = append(out.Left, LeftOut{Path: f.path, Why: "the seat " + seat + " is defined in more than one place: " + strings.Join(paths, ", ")})
|
||||
}
|
||||
continue
|
||||
}
|
||||
out.Seats = append(out.Seats, fs[0].raw)
|
||||
}
|
||||
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) {
|
||||
raw, err := fs.ReadFile(heads, p)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user