From 91791fdabde200e0255825d104cd272a2820f17c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 21:33:58 +0100 Subject: [PATCH] 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) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../cmd/mesh-delivery-indexer/named.go | 248 ++++++++---------- .../cmd/mesh-delivery-indexer/named_test.go | 55 +++- .../packages_integration_test.go | 51 ++-- .../cmd/mesh-delivery-indexer/seatdefs.go | 2 +- modules/mesh-delivery-indexer/go.mod | 1 + .../internal/index/candidates.go | 14 +- .../internal/index/order.go | 8 +- .../mesh-delivery-indexer/manifest_test.go | 23 +- modules/mesh-delivery-indexer/module.json | 8 +- 9 files changed, 204 insertions(+), 206 deletions(-) diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go index bb680dd8..36bd663b 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go @@ -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.`, 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, `.`, 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, `.`, or a seat's verb, `seat:.`, 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 /", 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 /", 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 /", 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 /", 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 /", 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 /", 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 /", 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 /", 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 diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go index 4d36c450..a434f66a 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go @@ -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) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go index 40e3abd3..59be4233 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go @@ -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) diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/seatdefs.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/seatdefs.go index 7255086c..44ca3c0e 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/seatdefs.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/seatdefs.go @@ -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, "") diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 3302bf36..86f555fb 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -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 diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 661ba3a2..32932c23 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -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) diff --git a/modules/mesh-delivery-indexer/internal/index/order.go b/modules/mesh-delivery-indexer/internal/index/order.go index e337c36a..d2f7fda1 100644 --- a/modules/mesh-delivery-indexer/internal/index/order.go +++ b/modules/mesh-delivery-indexer/internal/index/order.go @@ -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 diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index edf660e4..5cedc957 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -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) } } diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 30fd1d41..31aa4927 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -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",