Files
mesh-delivery/modules/mesh-delivery-forge-bridge/internal/bridge/call.go
T
jschoubben 0b597eb417 FB3, FB4, FB5: the bridge performs what the gate owes through the seat git and says each outcome, reads groups from the seat, and its owed tools
Taken over from the join (6ada2089, f1417ab0, 6e162a60, 3a4e62b0, 73fb42d7, ee15a280, a5476f62, 94a4e62a, 33907a81, e519cc87, e8f268c1) and fitted to the final
contracts: the seat's events are pull-updated, pull-merged, pull-closed, forge-delete; OwedEnded keeps only the fields §164 names; merge-pull
takes no ask. A merge act is performed only as a switched act (§169); from G the gate owes none (§167).

Builds: design 47 G §43, §45, §157, §161, §162, §163, §164, §167, §168, §169, §171, §250

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:54:56 +01:00

109 lines
4.6 KiB
Go

package bridge
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
)
// Caller calls a seat's verb by its manifest key (`seat:<seat>.<verb>`) with a JSON body and returns its answer: the
// bridge's process calls through the bus as its own principal, its tools bundle through the runtime (stdio.Ask).
type Caller func(ctx context.Context, key string, body any) (json.RawMessage, error)
// GitKey is the manifest key of a verb of the seat `git` (novox/hq design 47 *The seat `git`*): the bridge reaches the
// forge through the seat alone and names no module that holds it.
func GitKey(verb string) string { return "seat:" + git.Seat + "." + verb }
// askGit calls one verb of the seat `git` and decodes its answer into A. An answer the contract cannot type, or one
// that says an error, is the call's error: a failed read is never read as none (design 47 *A failed read answers an
// error*).
func askGit[A any](ctx context.Context, call Caller, verb string, in any) (A, error) {
var a A
raw, err := call(ctx, GitKey(verb), in)
if err != nil {
return a, fmt.Errorf("the seat git did not answer %s: %w", verb, err)
}
if err := json.Unmarshal(raw, &a); err != nil {
return a, fmt.Errorf("the seat git's answer of %s is not its contract's: %w", verb, err)
}
var said struct {
Error string `json:"error"`
}
_ = json.Unmarshal(raw, &said)
if said.Error != "" {
return a, fmt.Errorf("the seat git refused %s: %s", verb, said.Error)
}
return a, nil
}
// repository is `<owner>/<name>`, as the seat names a repository.
func repository(owner, repo string) string { return owner + "/" + repo }
// TreesVia asks the seat `git` for a commit's trees (verb trees).
func TreesVia(ctx context.Context, call Caller) Trees {
return func(owner, repo, sha string) (string, map[string]string, error) {
a, err := askGit[git.TreesAnswer](ctx, call, git.VerbTrees, git.TreesInput{Repository: repository(owner, repo), Commit: sha})
if err != nil {
return "", nil, err
}
if a.Root == "" {
return "", nil, fmt.Errorf("the seat git's trees of %s at %s name no root tree", repository(owner, repo), sha)
}
return a.Root, a.Trees, nil
}
}
// FilesVia asks the seat `git` for one file at a ref (verb file). A file the seat says is absent is ErrNotFound; an
// answer that says neither absent nor content is an error, never an empty file.
func FilesVia(ctx context.Context, call Caller) Files {
return func(owner, repo, path, ref string) ([]byte, error) {
raw, err := call(ctx, GitKey(git.VerbFile), git.FileInput{Repository: repository(owner, repo), Path: path, Ref: ref})
if err != nil {
return nil, fmt.Errorf("the seat git did not answer file: %w", err)
}
var a struct {
git.FileAnswer
Content *string `json:"content"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat git's answer of file is not its contract's: %w", err)
}
switch {
case a.Error != "":
return nil, fmt.Errorf("the seat git refused file %s at %s: %s", path, ref, a.Error)
case a.Absent:
return nil, fmt.Errorf("%w: %s at %s in %s", ErrNotFound, path, ref, repository(owner, repo))
case a.Content == nil:
return nil, fmt.Errorf("the seat git's file %s at %s says neither its content nor that it is absent", path, ref)
}
return []byte(*a.Content), nil
}
}
// errNoAnswer is a read whose answer carries nothing the contract says it must.
var errNoAnswer = errors.New("the seat git's answer carries nothing")
// MergedMessage is the merged message of a merge into the trunk, asked again: built as the bridge builds the one it writes
// on FORGE (it is that event's body through ConvertKept), from the pull request's owner, repository, number, branch, head
// and merge commit, which the seat git's verb pull answers. It reads the forge for every module directory (no kept inputs).
func MergedMessage(owner, repo string, number int, branch, head, merge string, trees Trees, files Files) (Message, error) {
body, err := json.Marshal(git.PullMergedEvent{Repository: repository(owner, repo), Number: int64(number), Branch: branch, Head: head, Base: Trunk, MergeCommit: merge})
if err != nil {
return Message{}, err
}
return ConvertKept(MergedSubject, body, trees, files, nil)
}
// splitRepository is a `<owner>/<name>` cut in two, refusing anything else.
func splitRepository(repository string) (string, string, error) {
owner, name, ok := strings.Cut(strings.TrimSpace(repository), "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", "", fmt.Errorf("repository %q is not <owner>/<name>", repository)
}
return owner, name, nil
}