Files
mesh-delivery/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go
T
jschoubben a600090fcd G-715: the forge bridge says a group's end once per base
ForgeGroupEnd gains base; the bridge reads each member's base from the seat's
verb groups (Member.base is already in the contract and seatgen's output),
keeps groups.<group>.<base>, says one end per base from that base's members,
and bridge_group_end_again takes the base.

Builds: design 47 G §715, A-236
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:19:46 +01:00

280 lines
13 KiB
Go

// Command mesh-delivery-forge-bridge-tools serves the forge bridge's tools (novox/hq ADR 0633 §4, issue 1585; design 51):
// the runtime launches it for a call. bridge_tags reads a package's git tags through the seat git's verb tags, for the
// builder's floor. bridge_groups reads the delivery groups the gate holds and what the bridge kept of their ends;
// bridge_group_end_again writes an ask into the bridge's state end-asks and waits for the bridge's answer there, since the
// bridge is the other program and serves no request subject (ADR 0437 §4).
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/bridge"
"mesh-delivery-forge-bridge/internal/groups"
)
// Tool names.
const (
GroupsTool = "bridge_groups"
EndAgainTool = "bridge_group_end_again"
// MergedTool answers the merged message of a merge on request (issue 1585).
MergedTool = "bridge_merged_message"
)
// asker writes an end-asks value and waits for the bridge's answer to it.
type asker func(key string, value []byte) (groups.EndAsk, error)
// Tools are the bridge's tools: groupsRead reads the seat mesh-delivery's groups with what the bridge kept of them.
func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string, number int) (json.RawMessage, error), forge bridge.Caller) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: bridge.TagsTool,
Description: "A package's git tags on the forge, for the builder's floor (design 51): every tag of repository whose name starts " +
"with prefix (go/v for a Go package in the go directory, v for one at the root), sorted. Read-only; read through the seat " +
"git's verb tags, at most " + fmt.Sprint(bridge.TagsMaxPages) + " pages (more says so).",
Input: map[string]any{"type": "object", "required": []string{"repository"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
"prefix": map[string]any{"type": "string", "description": "only tags starting with it"}}},
Run: func(args map[string]any) (any, error) {
return bridge.Tags(context.Background(), forge, str(args, "repository"), str(args, "prefix"))
}},
{Name: MergedTool,
Description: "The merged message of one merge, asked again (issue 1585): the message the bridge wrote on its stream FORGE when the " +
"pull request was merged into the trunk, built the same way from the forge (the merge commit's trees and the manifests " +
"and lock files of its module directories), the pull request's number, branch and head from the seat git's verb pull, and " +
"for a delivery group's member its group and members from the seat mesh-delivery's verb groups. For the indexer, which asks it when it takes in a held merge it does not hold in memory (a restart, a new " +
"build), and the ask-again a lost message lacked. About 0.5 MB for a repository of many modules. Answers {message} " +
"with the forge message whole, or why it cannot: the forge gives no merged pull request with that commit, or a seat did not answer. " +
"A merge made on the forge (a recovery merge, the operator's own) is read the same way.",
Input: map[string]any{"type": "object", "required": []string{"repository", "merge"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "the repository, owner/name, as the forge names it"},
"merge": map[string]any{"type": "string", "description": "the merge commit, 40 hex digits"},
"number": map[string]any{"type": "integer", "description": "the pull request's number, when known: the bridge reads that pull request and checks its merge commit"}}},
Run: func(args map[string]any) (any, error) {
number, _ := args["number"].(float64)
raw, err := merged(str(args, "repository"), str(args, "merge"), int(number))
if err != nil {
return nil, err
}
return map[string]any{"message": raw}, nil
}},
{Name: GroupsTool,
Description: "The delivery groups the seat mesh-delivery's verb groups answers (more than one member), each with its members' delivery " +
"state and whether each merged message is written on the forge stream (with the merge commit it named), and whether the " +
"group's end (forge.group.merged) is written: ended, or open with why (gathered, or every message written and its end " +
"not written yet). An open group with every member's message written is " +
"what bridge_group_end_again says the end of. Each group names its last end ask (who, why, when) and the bridge's " +
"answer to it, from the state end-asks. A group's name narrows it to that group; open lists only open ones.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "only this group, by its id (its branch)"},
"open": map[string]any{"type": "boolean", "description": "only the groups whose end is not written"}}},
Run: func(args map[string]any) (any, error) {
rows, unread, err := groupsRead()
if err != nil {
return nil, err
}
only, _ := args["open"].(bool)
group := str(args, "group")
kept := rows[:0]
for _, r := range rows {
if (group != "" && r.Group != group) || (only && !r.Open) {
continue
}
kept = append(kept, r)
}
return map[string]any{"groups": kept, "unread": unread}, nil
}},
{Name: EndAgainTool,
Description: "Say a delivery group's end (forge.group.merged) again, by hand, for a group whose end the indexer did not take " +
"in (a group bridge_groups names as merged with its end not written, or one the indexer waits for). The bridge " +
"writes it again from the seat mesh-delivery's groups, with the time appended to the message id so the stream does not drop it as " +
"a duplicate; the indexer takes an end for a group it already took in without changing anything. Refused while " +
"a member's merged message is not written. Answers whether it was written.",
Input: map[string]any{"type": "object", "required": []string{"group", "base"}, "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "the group, by its id (its branch)"},
"base": map[string]any{"type": "string", "description": "the watched branch its members merged into, whose end is said again"},
"why": map[string]any{"type": "string", "description": "why its end is said again, in plain words"}}},
Run: func(args map[string]any) (any, error) {
key, value, err := groups.NewEndAsk(str(args, "group"), str(args, "base"), str(args, "why"), stdio.CallerOf(args), now())
if err != nil {
return nil, err
}
a, err := ask(key, value)
if err != nil {
return nil, err
}
return map[string]any{"group": a.Group, "base": a.Base, "written": a.Written, "answer": a.Answer, "by": a.By, "why": a.Why}, nil
}},
}
}
// MergedOf is the merged message of the merge of repository at commit merge, as the bridge writes it: the pull request's
// number, branch and head through the seat git's verb pull (by number when given, else by the merge commit; a merge made
// on the forge is read the same way), the rest from the forge through trees and files (bridge.MergedMessage), and, for a
// member of a delivery group the seat mesh-delivery names (of), the group and its members as the process sets them.
func MergedOf(of []seat.Group, repository, merge string, number int, trees bridge.Trees, files bridge.Files, forge bridge.Caller) (json.RawMessage, error) {
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" {
return nil, fmt.Errorf("%q is not owner/name", repository)
}
fp, err := bridge.ForgeMade(context.Background(), forge, owner, repo, number, merge)
if err != nil {
return nil, fmt.Errorf("the forge gives no merged pull request of %s with the commit %s: %w", repository, merge, err)
}
msg, err := bridge.MergedMessage(owner, repo, fp.Number, fp.Branch, fp.Head, merge, fp.Base, trees, files)
if err != nil {
return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
}
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
return nil, err
}
if g, ok := groups.Named(of, f.Branch, f.Repository, f.Number); ok {
f.Group, f.Members = g.ID, groups.MembersOf(g)
}
return forgemsg.EncodeForge(f)
}
// read is the groups the seat mesh-delivery answers with what the bridge kept of them: the merged messages written and
// their ends in performed, and the last ask of each in end-asks with the bridge's answer.
func read(of []seat.Group, performed, asks stdio.KeptState) ([]groups.Row, []string, error) {
pkeys, err := performed.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the bridge's performed state cannot be listed: %w", err)
}
published, ended := map[string]string{}, map[string]string{}
for _, k := range pkeys {
into := ended
switch {
case strings.HasPrefix(k, "published."):
into = published
case !strings.HasPrefix(k, "groups."):
continue
}
if e, err := performed.Get(k); err == nil && e != nil {
into[k] = strings.TrimSpace(string(e.Value))
}
}
akeys, err := asks.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the bridge's state %s cannot be listed: %w", groups.EndAsksState, err)
}
askRaws := map[string][]byte{}
for _, k := range akeys {
if e, err := asks.Get(k); err == nil && e != nil {
askRaws[k] = e.Value
}
}
rows, unread := groups.Rows(of, published, ended, askRaws)
return rows, unread, nil
}
// askEnd writes the end-asks value at key and waits at most bound for the bridge's answer.
func askEnd(s stdio.KeptState, bound time.Duration) asker {
wait := askState(s, groups.EndAsksBucket, bound, func(v []byte) bool { a, err := groups.DecodeEndAsk(v); return err == nil && a.Done() })
return func(key string, value []byte) (groups.EndAsk, error) {
v, err := wait(key, value)
if err != nil {
return groups.EndAsk{}, err
}
return groups.DecodeEndAsk(v)
}
}
// askState writes an ask's value at key of the state s (its bucket named bucket) and waits, on a watch of the key, at most
// bound for the bridge's answer, a later value that done says is answered.
func askState(s stdio.KeptState, bucket string, bound time.Duration, done func([]byte) bool) func(key string, value []byte) ([]byte, error) {
return func(key string, value []byte) ([]byte, error) {
answered := make(chan []byte, 1)
var rev uint64
var mu sync.Mutex
take := func(revision uint64, v []byte) {
mu.Lock()
mine := rev
mu.Unlock()
if mine == 0 || revision <= mine || !done(v) {
return
}
select {
case answered <- v:
default:
}
}
if err := s.Watch(key, func(c stdio.StateChange) error {
if c.Op == "put" {
take(c.Revision, c.Value)
}
return nil
}); err != nil {
return nil, fmt.Errorf("watching %s.%s: %w", bucket, key, err)
}
r, err := s.Put(key, json.RawMessage(value))
if err != nil {
return nil, fmt.Errorf("the ask could not be written to %s: %w", bucket, err)
}
mu.Lock()
rev = r
mu.Unlock()
if e, err := s.Get(key); err == nil && e != nil {
take(e.Revision, e.Value)
}
wait, cancel := context.WithTimeout(context.Background(), bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel()
select {
case v := <-answered:
return v, nil
case <-wait.Done():
return nil, fmt.Errorf("the bridge did not answer within %s: its answer will be in %s.%s", bound, bucket, key)
}
}
}
// askCaller calls a tool as this bundle's module, through the runtime (stdio.Ask), the module's own invokes granting it.
func askCaller(_ context.Context, key string, body any) (json.RawMessage, error) {
return stdio.Ask(key, body)
}
func main() {
performed, asks := stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
tools := Tools(askEnd(asks, 25*time.Second), time.Now,
func() ([]groups.Row, []string, error) {
of, err := groups.Read(context.Background(), askCaller)
if err != nil {
return nil, nil, err
}
return read(of, performed, asks)
},
func(repository, merge string, number int) (json.RawMessage, error) {
ctx := context.Background()
of, err := groups.Read(ctx, askCaller)
if err != nil {
return nil, err
}
return MergedOf(of, repository, merge, number, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller), askCaller)
}, askCaller)
owedAsks := stdio.State(groups.OwedAsksState)
recorder := meshhandactsclient.Recorder{Ask: stdio.Ask}
tools = append(tools, OwedTools(askState(owedAsks, groups.OwedAsksBucket, 25*time.Second, func(v []byte) bool {
a, err := groups.DecodeOwedAsk(v)
return err == nil && a.Done()
}), func() ([]OwedRow, error) { return performedRows(performed) }, func(what, why string) error {
_, err := recorder.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "owed-dropped"})
return err
}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err)
os.Exit(1)
}
}