Files
mesh-catalog/modules/mesh-build-go/buildstep/generated.go
T
jschoubben 7b08f25d4a CAT-BG4/BN1: the seat generator moves into buildstep, one program for Go and TypeScript
Its version is the build module's own, so a new generator is a new build of
mesh-build-go and mesh-build-node and a new fingerprint of what they compare;
no repository of its own. Moved from seatgen c1fdd7c unchanged but for the
import path.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:12:46 +01:00

133 lines
5.0 KiB
Go

package buildstep
// **Regenerate and compare** (CAT-BG5, CAT-BN2; novox/hq ADR 0608 §5, design 26 *Contracts modules*). A module whose
// package artifact names `generated-from` holds contracts only: every file under its generated roots is written by the
// seat generator (package seatgen beside this file, command cmd/seatgen) from the definitions in that directory. A build module regenerates them with the generator
// it was built with (its version is this package's requirement, so a new generator is a new build module and a new
// fingerprint of what it builds) and names each file out of step: one that differs, one missing, one under a generated
// root the generator did not write. The test step fails on any; a trunk build's ask answers them in its out-of-step
// (delivery.BuildAnswer.OutOfStep), so the builder's build end raises generated-types-out-of-step even where no test
// step runs. Each build module compares its own roots: mesh-build-go `go`, mesh-build-node `ts`.
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/seatgen"
)
// GeneratedRootGo and GeneratedRootTS are the generated roots of a contracts module, one per build module.
const (
GeneratedRootGo = "go"
GeneratedRootTS = "ts"
)
var goModuleLine = regexp.MustCompile(`(?m)^module\s+(\S+)`)
// OutOfStep is every file under root, a path from the module's directory, that the generator writes otherwise than the
// module holds it, for each `generated-from` its package artifacts name, sorted and each once; none for a module naming
// none. The generated Go tree's module path is read from <from>/go/go.mod and the TypeScript package's name from
// <from>/ts/package.json, as the contracts module states them.
func OutOfStep(moduleDir, root string) ([]string, error) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if errors.Is(err, os.ErrNotExist) {
return nil, nil // no manifest here: nothing names generated-from
}
if err != nil {
return nil, fmt.Errorf("the module's manifest cannot be read for its generated files: %w", err)
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("the module's manifest does not parse: %w", err)
}
froms := map[string]bool{}
if m.Build != nil {
for _, a := range m.Build.Artifacts {
if a.GeneratedFrom != "" {
froms[filepath.Clean(a.GeneratedFrom)] = true
}
}
}
seen := map[string]bool{}
for from := range froms {
if filepath.IsAbs(from) || from == ".." || strings.HasPrefix(from, "../") {
return nil, fmt.Errorf("generated-from %q is not inside the module", from)
}
dir := filepath.Join(moduleDir, from)
goModule, tsPackage, err := generatedNames(dir)
if err != nil {
return nil, err
}
g, err := seatgen.Load(dir, goModule, tsPackage)
if err != nil {
return nil, fmt.Errorf("the seat definitions in %s do not load: %w", from, err)
}
files, err := seatgen.Generate(g)
if err != nil {
return nil, fmt.Errorf("the seat definitions in %s do not generate: %w", from, err)
}
out, err := seatgen.Compare(dir, files)
if err != nil {
return nil, err
}
for _, line := range out {
p, _, _ := strings.Cut(line, ": ")
if p == root || strings.HasPrefix(p, root+"/") {
seen[filepath.ToSlash(filepath.Join(from, p))] = true
}
}
}
paths := make([]string, 0, len(seen))
for p := range seen {
paths = append(paths, p)
}
sort.Strings(paths)
return paths, nil
}
func generatedNames(dir string) (goModule, tsPackage string, err error) {
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootGo, "go.mod")); err == nil {
if m := goModuleLine.FindSubmatch(raw); m != nil {
goModule = string(m[1])
}
}
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootTS, "package.json")); err == nil {
var p struct {
Name string `json:"name"`
}
if json.Unmarshal(raw, &p) == nil {
tsPackage = p.Name
}
}
if goModule == "" || tsPackage == "" {
return "", "", errors.New("a contracts module names its generated Go module in go/go.mod and its TypeScript package in ts/package.json; " +
filepath.Base(dir) + " names " + map[bool]string{true: "not the TypeScript package", false: "not the Go module"}[goModule != ""])
}
return goModule, tsPackage, nil
}
// CompareStep is AskSteps.Compare for a build module comparing root: it reads {"workspace", "module"}, the workspace's
// tree where tree says it is (the build module's own layout), and answers {"out-of-step": [...]}.
func CompareStep(root string, tree func(workspace string) string) StepFunc {
return func(_ context.Context, args map[string]any) (any, error) {
ws, _ := args["workspace"].(string)
mod, _ := args["module"].(string)
if ws == "" {
return nil, Refuse("the compare names no workspace")
}
paths, err := OutOfStep(filepath.Join(tree(ws), filepath.FromSlash(mod)), root)
if err != nil {
return nil, err
}
return map[string]any{"out-of-step": paths}, nil
}
}