The indexer reads the forge through the seat git alone: trees, file, contains and open-pulls, an answer's error a failure (milestone 47/G W4.15a)

The forge module's own tools leave the program and its invokes. The seat's Go contract is feat/47-G-seats' (contracts/go/seats/git); this branch builds against it at the join.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

Builds: design 47 G §310, §312, §317, §428
This commit is contained in:
2026-11-11 14:32:33 +01:00
parent 42785f6fa2
commit 91791fdabd
9 changed files with 204 additions and 206 deletions
@@ -11,6 +11,7 @@ import (
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
@@ -18,37 +19,75 @@ import (
"mesh-delivery-indexer/internal/index"
)
// commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea): with `paths`, by module directory
// (the root spelled ""), the paths relative to it, it answers {root, trees, paths} with paths by directory then path, each
// path's git object id; a path not at the commit fails the call naming it (the interface agreed in R4e; mesh-sdk
// forgemsg.Forge.Paths; novox/hq ADR 0499 clause 3).
const commitTreesTool = "gitea.gitea_commit_trees"
// The forge is read through the seat git alone (design 47 *The seat git*; milestone 47/G W4.15): each read is the seat's
// verb, `seat:git.<verb>`, its input and answer the seat's contract; an answer that carries an error is a failure,
// never an empty answer.
// toolCaller calls a module's tool, `<module>.<tool>`, as the indexer's own account (mesh-sdk process.Conn.Call).
// gitVerb is the address of the seat git's verb.
func gitVerb(verb string) string { return "seat:" + git.Seat + "." + verb }
// toolCaller calls a module's tool, `<module>.<tool>`, or a seat's verb, `seat:<seat>.<verb>`, as the indexer's own account
// (mesh-sdk process.Conn.Call).
type toolCaller func(key string, body any) (json.RawMessage, error)
// namedVia asks the forge for the objects of the named paths through call. An answer without the object of every path
// asked is an error naming it.
// askGit asks one verb of the seat git and decodes its answer into out; an answer whose error is set is the failure it
// names, with its kind.
func askGit(call toolCaller, verb string, in, out any) error {
raw, err := call(gitVerb(verb), in)
if err != nil {
return err
}
var failed struct {
Error string `json:"error"`
ErrorKind git.ErrorKind `json:"error-kind"`
}
if err := json.Unmarshal(raw, &failed); err != nil {
return fmt.Errorf("the answer of %s is not its seat's answer: %w", gitVerb(verb), err)
}
if failed.Error != "" {
return fmt.Errorf("%s (%s): %s", gitVerb(verb), failed.ErrorKind, failed.Error)
}
if err := json.Unmarshal(raw, out); err != nil {
return fmt.Errorf("the answer of %s is not its seat's answer: %w", gitVerb(verb), err)
}
return nil
}
// treesOf asks the seat git's trees of a commit.
func treesOf(call toolCaller, repository string, in git.TreesInput) (git.TreesAnswer, error) {
var a git.TreesAnswer
in.Repository = trunk.RepositoryName(repository)
if !strings.Contains(in.Repository, "/") {
return a, fmt.Errorf("%q is not <owner>/<name>", repository)
}
return a, askGit(call, git.VerbTrees, in, &a)
}
// fileOf asks the seat git's file at a ref: its content, or absent when the forge holds no such file there.
func fileOf(call toolCaller, repository, p, ref string) (content []byte, absent bool, err error) {
var a git.FileAnswer
in := git.FileInput{Repository: trunk.RepositoryName(repository), Path: p, Ref: ref}
if !strings.Contains(in.Repository, "/") {
return nil, false, fmt.Errorf("%q is not <owner>/<name>", repository)
}
if err := askGit(call, git.VerbFile, in, &a); err != nil {
return nil, false, err
}
return []byte(a.Content), a.Absent, nil
}
// namedVia asks the forge for the objects of the named paths (the seat git's trees with paths). An answer without the
// object of every path asked is an error naming it.
func namedVia(call toolCaller) func(ctx context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
return func(_ context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha, "paths": paths})
a, err := treesOf(call, repository, git.TreesInput{Commit: sha, Paths: paths})
if err != nil {
return nil, err
}
var a struct {
Paths map[string]map[string]string `json:"paths"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the answer of %s is not {root, trees, paths}: %w", commitTreesTool, err)
}
for dir, want := range paths {
for _, p := range want {
if a.Paths[dir][p] == "" {
return nil, fmt.Errorf("the answer of %s names no object for %q of %q", commitTreesTool, p, dir)
return nil, fmt.Errorf("the answer of %s names no object for %q of %q", gitVerb(git.VerbTrees), p, dir)
}
}
}
@@ -56,24 +95,14 @@ func namedVia(call toolCaller) func(ctx context.Context, repository, sha string,
}
}
// trunkVia asks the forge for the module directories of a commit (issue 1900): the keys of the `trees` the same tool
// answers without paths, one per directory that holds a module.json (the root as ""; the tool lists no other).
// trunkVia asks the forge for the module directories of a commit (issue 1900): the keys of the trees the seat git answers
// without paths, one per directory that holds a module.json (the root as "").
func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string) ([]string, error) {
return func(_ context.Context, repository, sha string) ([]string, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha})
a, err := treesOf(call, repository, git.TreesInput{Commit: sha})
if err != nil {
return nil, err
}
var a struct {
Trees map[string]string `json:"trees"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err)
}
dirs := make([]string, 0, len(a.Trees))
for dir := range a.Trees {
dirs = append(dirs, dir)
@@ -82,36 +111,25 @@ func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string)
}
}
// filesVia asks the forge for every blob under each directory at a commit (the same tool with `files`, ADR 0593 §2): it
// answers {files} by directory, then path relative to it, each with its blob's object id. An answer that lists no
// directory asked is an error naming it; a cut listing fails the call in the tool.
// filesVia asks the forge for every blob under each directory at a commit (the seat git's trees with files, ADR 0593 §2),
// by directory, then path relative to it. An answer that lists no directory asked is an error naming it.
func filesVia(call toolCaller) func(ctx context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
return func(_ context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha, "files": dirs})
a, err := treesOf(call, repository, git.TreesInput{Commit: sha, Files: dirs})
if err != nil {
return nil, err
}
var a struct {
Files map[string]map[string]string `json:"files"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the answer of %s is not {files}: %w", commitTreesTool, err)
}
for _, d := range dirs {
if _, ok := a.Files[d]; !ok {
return nil, fmt.Errorf("the answer of %s lists no files of %q: %w", commitTreesTool, d, index.ErrNotInTree)
return nil, fmt.Errorf("the answer of %s lists no files of %q: %w", gitVerb(git.VerbTrees), d, index.ErrNotInTree)
}
}
return a.Files, nil
}
}
// goSumVia reads a module directory's go.sum at a commit (gitea_get_file): the version line's packages of a module
// whose build reported no imports (ADR 0593 §2). A directory with no go.sum answers nothing.
// goSumVia reads a module directory's go.sum at a commit: the version line's packages of a module whose build reported
// no imports (ADR 0593 §2). A directory with no go.sum answers nothing.
func goSumVia(call toolCaller) func(ctx context.Context, repository, sha, dir string) ([]byte, error) {
return goFileVia(call, "go.sum")
}
@@ -122,14 +140,10 @@ func goModVia(call toolCaller) func(ctx context.Context, repository, sha, dir st
return goFileVia(call, "go.mod")
}
// goFileVia reads one Go file of a module directory at a commit through the forge's get-file tool; a file the commit does
// not hold is none.
// goFileVia reads one Go file of a module directory at a commit through the seat git's file; a file the forge says the
// commit does not hold is none.
func goFileVia(call toolCaller, name string) func(ctx context.Context, repository, sha, dir string) ([]byte, error) {
return func(_ context.Context, repository, sha, dir string) ([]byte, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
p := name
switch {
case name == "": // readFileVia: dir is the file's whole path
@@ -137,107 +151,69 @@ func goFileVia(call toolCaller, name string) func(ctx context.Context, repositor
case dir != "" && dir != meshcatalogclient.RootPath:
p = dir + "/" + name
}
raw, err := call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": p, "ref": sha})
if err != nil {
if strings.Contains(err.Error(), ": 404 ") {
return nil, nil
}
content, absent, err := fileOf(call, repository, p, sha)
if err != nil || absent {
return nil, err
}
var f struct {
File *struct {
Content *string `json:"content"`
} `json:"file"`
}
if err := json.Unmarshal(raw, &f); err != nil || f.File == nil || f.File.Content == nil {
return nil, fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err)
}
return []byte(*f.File.Content), nil
return content, nil
}
}
// containsVia asks the forge whether a commit is in another (gitea.gitea_contains: contains is true for the same commit or an ancestor).
// containsVia asks the forge whether a commit is in another (the seat git's contains: true for the same commit or an
// ancestor).
func containsVia(call toolCaller) func(ctx context.Context, repository, commit, in string) (bool, error) {
return func(_ context.Context, repository, commit, in string) (bool, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
r := trunk.RepositoryName(repository)
if !strings.Contains(r, "/") {
return false, fmt.Errorf("%q is not <owner>/<name>", repository)
}
raw, err := call("gitea.gitea_contains", map[string]any{"owner": owner, "repo": repo, "commit": commit, "in": in})
if err != nil {
var a git.ContainsAnswer
if err := askGit(call, git.VerbContains, git.ContainsInput{Repository: r, Commit: commit, In: in}, &a); err != nil {
return false, err
}
var a struct {
Contains *bool `json:"contains"`
}
if err := json.Unmarshal(raw, &a); err != nil || a.Contains == nil {
return false, fmt.Errorf("the answer of gitea.gitea_contains names no contains: %v", err)
}
return *a.Contains, nil
return a.Contains, nil
}
}
// getFileTool is the forge's tool for one file at a ref (mesh-catalog modules/gitea gitea_get_file): arguments owner,
// repo, path and ref, answer {file: {content}}; a path it does not have fails with a 404 in its words.
const getFileTool = "gitea.gitea_get_file"
// lockFiles are read beside a manifest, as the forge bridge reads them (bridge.LockFiles).
var lockFiles = []string{"go.sum", "package-lock.json"}
// readVia reads a repository at a commit as the forge bridge does for a merged message: the trees by gitea_commit_trees,
// then the module.json and lock files of the one directory dir by gitea_get_file (a repository can hold hundreds). A manifest the
// forge does not have is an error; a directory with no module.json in the trees is answered with no manifest; a lock file it does not have is left out.
// readVia reads a repository at a commit as the forge bridge does for a merged message: the seat git's trees, then the
// module.json and lock files of the one directory dir by its file (a repository can hold hundreds). A manifest the forge
// does not have is an error; a directory with no module.json in the trees is answered with no manifest; a lock file it does
// not have is left out.
func readVia(call toolCaller) func(ctx context.Context, repository, sha, dir string) (index.RepoRead, error) {
return func(_ context.Context, repository, sha, dir string) (index.RepoRead, error) {
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return index.RepoRead{}, fmt.Errorf("%q is not <owner>/<name>", repository)
}
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha})
a, err := treesOf(call, repository, git.TreesInput{Commit: sha})
if err != nil {
return index.RepoRead{}, err
}
var a struct {
Root string `json:"root"`
Trees map[string]string `json:"trees"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return index.RepoRead{}, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err)
}
out := index.RepoRead{Root: a.Root, Trees: a.Trees, Manifests: map[string]string{}, Locks: map[string]map[string]string{}}
file := func(dir, name string) (string, error) {
file := func(dir, name string) (string, bool, error) {
p := name
if dir != "" {
p = dir + "/" + name
}
raw, err := call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": p, "ref": sha})
if err != nil {
return "", err
}
var f struct {
File *struct {
Content *string `json:"content"`
} `json:"file"`
}
if err := json.Unmarshal(raw, &f); err != nil || f.File == nil || f.File.Content == nil {
return "", fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err)
}
return *f.File.Content, nil
content, absent, err := fileOf(call, repository, p, sha)
return string(content), absent, err
}
if _, held := a.Trees[dir]; held {
m, err := file(dir, "module.json")
m, absent, err := file(dir, "module.json")
if err == nil && absent {
err = fmt.Errorf("the forge holds no such file at %.12s", sha)
}
if err != nil {
return index.RepoRead{}, fmt.Errorf("%s: %w", path.Join(dir, "module.json"), err)
}
out.Manifests[dir] = m
for _, name := range lockFiles {
c, err := file(dir, name)
c, absent, err := file(dir, name)
if err != nil {
if strings.Contains(err.Error(), ": 404 ") {
continue
}
return index.RepoRead{}, fmt.Errorf("%s: %w", path.Join(dir, name), err)
}
if absent {
continue
}
sum := sha256.Sum256([]byte(c))
if out.Locks[dir] == nil {
out.Locks[dir] = map[string]string{}
@@ -257,34 +233,24 @@ func readVia(call toolCaller) func(ctx context.Context, repository, sha, dir str
}
}
// The forge's tool for the open pull requests, and the trunk branch they are open into.
const (
openPullsTool = "gitea.gitea_open_pulls"
trunkBranch = "main"
)
// trunkBranch is the branch pull requests are open into.
const trunkBranch = "main"
// openVia asks the forge which pull requests into the trunk are open now (gitea_open_pulls), every repository's.
// openVia asks the forge which pull requests into the trunk are open now (the seat git's open-pulls), every repository's.
// An answer that lacks the list is refused, never read as none.
func openVia(call toolCaller) func(ctx context.Context) ([]forgemsg.OpenPull, error) {
return func(context.Context) ([]forgemsg.OpenPull, error) {
raw, err := call(openPullsTool, map[string]any{})
if err != nil {
var o git.OpenPullsAnswer
if err := askGit(call, git.VerbOpenPulls, git.OpenPullsInput{}, &o); err != nil {
return nil, err
}
var o struct {
Pulls *[]struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Number int `json:"number"`
Base string `json:"base"`
} `json:"pulls"`
}
if err := json.Unmarshal(raw, &o); err != nil || o.Pulls == nil {
return nil, fmt.Errorf("the answer of %s is not {pulls}: %v", openPullsTool, err)
if o.Pulls == nil {
return nil, fmt.Errorf("the answer of %s lists no pulls", gitVerb(git.VerbOpenPulls))
}
out := []forgemsg.OpenPull{}
for _, p := range *o.Pulls {
if p.Base == trunkBranch && p.Number > 0 {
out = append(out, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
for _, p := range o.Pulls {
if p.Pull.Base == trunkBranch && p.Pull.Number > 0 {
out = append(out, forgemsg.OpenPull{Repository: p.Pull.Repository, Number: int(p.Pull.Number)})
}
}
return out, nil
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"mesh-delivery-indexer/internal/index"
"slices"
"strings"
@@ -11,20 +12,48 @@ import (
)
func TestNamedViaAsksTheTreesToolWithThePathsAndTakesTheirObjects(t *testing.T) { // I6, ADR 0499 clause 3
var asked map[string]any
var asked git.TreesInput
call := func(key string, body any) (json.RawMessage, error) {
if key != commitTreesTool {
if key != "seat:git.trees" {
t.Errorf("called %s", key)
}
asked = body.(map[string]any)
asked = body.(git.TreesInput)
return json.RawMessage(`{"root":"r0","trees":{"":"r0"},"paths":{"":{"src":"1111111111111111111111111111111111111111","package.json":"3333333333333333333333333333333333333333"}}}`), nil
}
got, err := namedVia(call)(context.Background(), "novox/mesh-sdk", "s", map[string][]string{"": {"package.json", "src"}})
if err != nil || got[""]["src"] != "1111111111111111111111111111111111111111" || got[""]["package.json"] == "" {
t.Fatalf("objects = %v, %v", got, err)
}
if asked["owner"] != "novox" || asked["repo"] != "mesh-sdk" || asked["sha"] != "s" || asked["paths"] == nil {
t.Errorf("asked %v, want the owner, repo, commit and paths", asked)
if asked.Repository != "novox/mesh-sdk" || asked.Commit != "s" || asked.Paths == nil {
t.Errorf("asked %v, want the repository, commit and paths", asked)
}
// An answer that carries the seat's error is the failure it names, never an empty answer.
failed := func(string, any) (json.RawMessage, error) {
return json.RawMessage(`{"error":"the forge did not answer","error-kind":"failed"}`), nil
}
if _, err := namedVia(failed)(context.Background(), "novox/mesh-sdk", "s", nil); err == nil || !strings.Contains(err.Error(), "the forge did not answer") {
t.Errorf("an error answer: %v, want its failure", err)
}
for _, row := range []struct {
name, answer string
want int
refused bool
}{
{"one open into the trunk, one into another branch", `{"pulls":[{"pull":{"repository":"novox/a","number":3,"base":"main","branch":"b","head":"h"}},{"pull":{"repository":"novox/a","number":4,"base":"dev","branch":"c","head":"h"}}]}`, 1, false},
{"none open", `{"pulls":[]}`, 0, false},
{"no list", `{}`, 0, true},
{"an error", `{"error":"refused","error-kind":"refused"}`, 0, true},
} {
open := func(key string, _ any) (json.RawMessage, error) {
if key != "seat:git.open-pulls" {
t.Errorf("called %s", key)
}
return json.RawMessage(row.answer), nil
}
got, err := openVia(open)(context.Background())
if (err != nil) != row.refused || len(got) != row.want {
t.Errorf("open-pulls %s: %v %v, want %d, refused %v", row.name, got, err, row.want, row.refused)
}
}
call = func(string, any) (json.RawMessage, error) {
return json.RawMessage(`{"root":"r0","trees":{},"paths":{"":{"src":"1111111111111111111111111111111111111111"}}}`), nil
@@ -48,7 +77,7 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3
if !slices.Equal(dirs, []string{"modules/a", "modules/b"}) {
t.Errorf("dirs = %v", dirs)
}
if _, has := gotArgs.(map[string]any)["paths"]; has {
if gotArgs.(git.TreesInput).Paths != nil {
t.Error("the trunk asked for paths")
}
// A directory the answer lists no files of is a failure of the tree (index.ErrNotInTree); a call that fails is not.
@@ -114,17 +143,17 @@ func TestReadViaReadsManifestsAndLocks(t *testing.T) {
var files []string
call := func(key string, body any) (json.RawMessage, error) {
switch key {
case commitTreesTool:
case "seat:git.trees":
return json.RawMessage(`{"root":"r0","trees":{"":"t1","modules/other":"t2"}}`), nil
case getFileTool:
files = append(files, body.(map[string]string)["path"])
switch body.(map[string]string)["path"] {
case "seat:git.file":
files = append(files, body.(git.FileInput).Path)
switch body.(git.FileInput).Path {
case "module.json":
return json.RawMessage(`{"file":{"content":"{\"module\":\"snake\"}"}}`), nil
return json.RawMessage(`{"path":"module.json","content":"{\"module\":\"snake\"}"}`), nil
case "go.sum":
return json.RawMessage(`{"file":{"content":"x"}}`), nil
return json.RawMessage(`{"path":"go.sum","content":"x"}`), nil
}
return nil, errors.New("Gitea API /x: 404 not found")
return json.RawMessage(`{"path":"x","absent":true}`), nil
}
return nil, errors.New("unexpected " + key)
}
@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"errors"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"os"
"strings"
"testing"
@@ -419,70 +420,64 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
}
}
// seatFiles are mesh-contracts' files by commit, as gitea_get_file answers them.
// seatFiles are mesh-contracts' files by commit, as the seat git's file answers them.
var seatFiles = map[string]map[string]string{}
// serveForge answers the forge's tools on the bus as the gitea module does: gitea_commit_trees (the trees of a commit,
// or with files the blobs under each directory of mesh-sdk's listing at that commit), gitea_get_file (every go.sum
// holds goSum) and gitea_contains.
// serveForge answers the seat git's verbs on the bus as its holder, the forge module, does (milestone 47/G W4.15): trees
// (the trees of a commit, or with files the blobs under each directory of mesh-sdk's listing at that commit), file (every
// go.sum holds goSum; a seat definition the commit does not hold is absent) and contains.
func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]string, goSum string) {
t.Helper()
reply := func(m *nats.Msg, result any) {
raw, _ := json.Marshal(map[string]any{"result": result, "node": "n0"})
_ = m.Respond(raw)
}
trees, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) {
var a struct {
Repo string `json:"repo"`
Sha string `json:"sha"`
Files []string `json:"files"`
}
trees, err := nc.Subscribe("mesh.seat.git.tool.trees", func(m *nats.Msg) {
var a git.TreesInput
_ = json.Unmarshal(m.Data, &a)
out := map[string]any{"root": "r0", "trees": map[string]string{}}
out := git.TreesAnswer{Root: "r0", Trees: map[string]string{}, Commit: a.Commit}
if len(a.Files) > 0 {
files := map[string]map[string]string{}
out.Files = map[string]map[string]string{}
for _, d := range a.Files {
if a.Repo == "mesh-sdk" && d == "go" && listing[a.Sha] != nil {
files[d] = listing[a.Sha]
if a.Repository == "novox/mesh-sdk" && d == "go" && listing[a.Commit] != nil {
out.Files[d] = listing[a.Commit]
}
if fs, ok := seatFiles[a.Sha]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
files[d] = map[string]string{}
if fs, ok := seatFiles[a.Commit]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
out.Files[d] = map[string]string{}
for p := range fs {
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
files[d][rel] = "blob"
out.Files[d][rel] = "blob"
}
}
}
}
out["files"] = files
}
reply(m, out)
})
if err != nil {
t.Fatal(err)
}
file, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
var a struct{ Repo, Path, Ref string }
file, err := nc.Subscribe("mesh.seat.git.tool.file", func(m *nats.Msg) {
var a git.FileInput
_ = json.Unmarshal(m.Data, &a)
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is absent
if v, ok := seatFiles[a.Ref][a.Path]; ok {
reply(m, map[string]any{"file": map[string]any{"content": v}})
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Content: v})
} else {
raw, _ := json.Marshal(map[string]any{"error": "gitea: 404 Not Found", "node": "n0"})
_ = m.Respond(raw)
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Absent: true})
}
return
}
reply(m, map[string]any{"file": map[string]any{"content": goSum}})
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Content: goSum})
})
if err != nil {
t.Fatal(err)
}
// Each merge here is newer than the one before: a commit is in another only when it is that commit.
contains, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_contains", func(m *nats.Msg) {
var a struct{ Commit, In string }
contains, err := nc.Subscribe("mesh.seat.git.tool.contains", func(m *nats.Msg) {
var a git.ContainsInput
_ = json.Unmarshal(m.Data, &a)
reply(m, map[string]any{"contains": a.Commit == a.In})
reply(m, git.ContainsAnswer{Commit: a.Commit, In: a.In, Contains: a.Commit == a.In})
})
if err != nil {
t.Fatal(err)
@@ -21,7 +21,7 @@ import (
// checkerAsksStream is the checker's stream asks as the bus names it (its manifest's streams; StreamBusName).
const checkerAsksStream = "ASKS"
// readFileVia reads one file of a repository at a commit through the forge's get-file tool; a path the commit does not
// readFileVia reads one file of a repository at a commit through the seat git's file; a path the commit does not
// hold is nil with no error.
func readFileVia(call toolCaller) func(ctx context.Context, repository, sha, path string) ([]byte, error) {
return goFileVia(call, "")
+1
View File
@@ -10,6 +10,7 @@ require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/git v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
@@ -92,7 +92,7 @@ type Indexer struct {
Packages Store
// SeatDefinitions is the state seat-definitions (seatdefs.go; design 26, DLV-I17); nil keeps none.
SeatDefinitions Store
// ReadFile reads one file of a repository at a commit (gitea.gitea_get_file); a file the commit does not hold is
// ReadFile reads one file of a repository at a commit (the seat git's file); a file the commit does not hold is
// nil with no error. Nil reads no seat definition.
ReadFile func(ctx context.Context, repository, sha, path string) ([]byte, error)
// SaySeatDefinition says seat-definition-changed (seatdefs.go); nil says nothing.
@@ -108,13 +108,13 @@ type Indexer struct {
// Graph is the module graph's state (GraphBucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
Graph Store
// Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each
// with its git object id (gitea.gitea_commit_trees with files): the listing a package's digest is computed from.
// with its git object id (the seat git's trees with files): the listing a package's digest is computed from.
// Nil publishes no package.
Files func(ctx context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error)
// GoSum reads a module directory's go.sum at a commit (gitea.gitea_get_file): the mesh Go modules a module with no
// GoSum reads a module directory's go.sum at a commit (the seat git's file): the mesh Go modules a module with no
// reported import set takes the version line of. Nil reads none: such a module keeps what it held.
GoSum func(ctx context.Context, repository, sha, dir string) ([]byte, error)
// GoMod reads a module directory's go.mod at a commit (gitea.gitea_get_file): the mesh Go modules of its own
// GoMod reads a module directory's go.mod at a commit (the seat git's file): the mesh Go modules of its own
// repository it replaces by a directory (fingerprint.ReplacedSiblings), which the version line takes by their
// content; nil reads none.
GoMod func(ctx context.Context, repository, sha, dir string) ([]byte, error)
@@ -122,7 +122,7 @@ type Indexer struct {
Now func() time.Time
Builds *Builds
// Named asks the forge for the git object of each named path of the module directories given, at a commit
// (gitea.gitea_commit_trees with paths; novox/hq ADR 0499 clauses 2 and 3): the indexer is the one reader of a
// (the seat git's trees with paths; novox/hq ADR 0499 clauses 2 and 3): the indexer is the one reader of a
// manifest's paths (ADR 0495 clause 1, the lead's decision L8 of R4e). Nil asks nothing, and a module naming paths
// has none.
Named func(ctx context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error)
@@ -130,10 +130,10 @@ type Indexer struct {
// Read asks the forge for a repository's trees at a commit and the manifest and lock-file hashes of the one module directory dir (the
// root spelled ""), as the forge bridge reads them for a merged message (registered.go). Nil indexes no registered module.
Read func(ctx context.Context, repository, sha, dir string) (RepoRead, error)
// Contains says whether a commit is the second or an ancestor of it (gitea.gitea_contains).
// Contains says whether a commit is the second or an ancestor of it (the seat git's contains).
Contains func(ctx context.Context, repository, commit, in string) (bool, error)
Logf func(format string, args ...any)
// ReadOpen asks the forge which pull requests into the trunk are open now (gitea_open_pulls). A catalogue pull message
// ReadOpen asks the forge which pull requests into the trunk are open now (the seat git's open-pulls). A catalogue pull message
// that finds the candidates of another pull request asks it and deletes those of a pull request no longer open, so a
// closed message the indexer missed heals at the next one (issue 1126); nil asks none.
ReadOpen func(ctx context.Context) ([]forgemsg.OpenPull, error)
@@ -53,10 +53,10 @@ func newMergeWrite(f forgemsg.Forge) *mergeWrite {
return &mergeWrite{repository: f.Repository, merge: f.Merge}
}
// notFound says the forge answered that a commit it was asked about does not exist: gitea's status ": 404 ", matched as
// readVia matches it (cmd/mesh-delivery-indexer named.go). A bare "404" is not enough: the error's words carry both shas,
// and a 502 naming a sha with 404 in it would read as not older and write a late message backwards. The thorough form,
// a typed not-found answered by gitea_contains, is noted on issue 1126.
// notFound says the forge answered that a commit it was asked about does not exist: the seat git's contains answers it as
// an error of kind failed whose words carry the forge's status ": 404 " (cmd/mesh-delivery-indexer named.go). A bare "404"
// is not enough: the error's words carry both shas, and a 502 naming a sha with 404 in it would read as not older and
// write a late message backwards. The thorough form, a typed not-found in the seat's error kinds, is noted on issue 1126.
func notFound(err error) bool { return err != nil && strings.Contains(err.Error(), ": 404 ") }
// older says commit is older than later: the forge says commit is in later and the two differ. A commit the forge does not
+15 -8
View File
@@ -3,6 +3,7 @@ package manifest
import (
"bytes"
"encoding/json"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"io/fs"
"os"
"path/filepath"
@@ -169,7 +170,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
// commit is in another (issue 1900).
if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
if want := []string{"mesh-delivery-assigner.assignments", "seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
@@ -385,14 +386,16 @@ func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1238/AC9 (ADR
}
}
// Every forge tool the program calls is granted in invokes, or the bus refuses the call (issue 1900).
// Every forge read the program makes is a verb of the seat git granted in invokes, or the bus refuses the call (issue
// 1900); no program file names the forge module's own tools (milestone 47/G W4.15, design 47 *The seat git*).
func TestEveryGiteaToolTheCodeCallsIsInInvokes(t *testing.T) {
m, _ := load(t)
files, err := filepath.Glob("cmd/mesh-delivery-indexer/*.go")
if err != nil || len(files) == 0 {
t.Fatalf("no program files: %v", err)
}
re := regexp.MustCompile(`gitea\.gitea_[a-z_]+`)
verbs := map[string]string{"VerbTrees": git.VerbTrees, "VerbFile": git.VerbFile, "VerbContains": git.VerbContains, "VerbOpenPulls": git.VerbOpenPulls}
forge, asked := regexp.MustCompile(`gitea\.gitea_[a-z_]+`), regexp.MustCompile(`git\.Verb[A-Za-z]+`)
seen := 0
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
@@ -402,14 +405,18 @@ func TestEveryGiteaToolTheCodeCallsIsInInvokes(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, tool := range re.FindAllString(string(src), -1) {
for _, tool := range forge.FindAllString(string(src), -1) {
t.Errorf("%s calls the forge module's own tool %s, not the seat git", f, tool)
}
for _, v := range asked.FindAllString(string(src), -1) {
seen++
if !slices.Contains(m.Invokes, tool) {
t.Errorf("%s calls %s, which invokes does not grant", f, tool)
verb, ok := verbs[strings.TrimPrefix(v, "git.")]
if !ok || !slices.Contains(m.Invokes, "seat:"+git.Seat+"."+verb) {
t.Errorf("%s asks %s, which invokes does not grant", f, v)
}
}
}
if seen < 2 {
t.Errorf("found %d tool names in the program, want at least the two it calls", seen)
if seen < 4 {
t.Errorf("found %d verbs of the seat git in the program, want at least the four it asks", seen)
}
}
+4 -4
View File
@@ -121,10 +121,10 @@
],
"invokes": [
"mesh-delivery-assigner.assignments",
"gitea.gitea_commit_trees",
"gitea.gitea_contains",
"gitea.gitea_get_file",
"gitea.gitea_open_pulls",
"seat:git.trees",
"seat:git.file",
"seat:git.contains",
"seat:git.open-pulls",
"mesh-delivery-checker.checker_tries",
"mesh-delivery-forge-bridge.bridge_merged_message",
"seat:node-engine.composition",