The gate reads what the forge would merge now from the seat git's pull, so it names no forge tool (milestone 47/G, W11.18 follow-up)

The base's protection and required contexts, the forge's conflict word and the merging
account's write access come from the pull answer's merging (issues 1275, 1945; ADR 0441 §1);
a required context not a success at the commit blocks.

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

(Joined from 13e12d2c.)
Builds: design 47 G §252, §310, §312 (GATE14)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:37:33 +01:00
parent eb7a25622d
commit a9fa004758
5 changed files with 67 additions and 54 deletions
@@ -363,7 +363,8 @@ func (r *runtime) answer(m rpcMessage) {
for _, p := range pulls {
if p.Owner+"/"+p.Repo == repository && p.Number == int(number) {
return gitseat.PullAnswer{Pull: &gitseat.Pull{Repository: repository, Number: int64(p.Number), Base: p.Base,
Head: p.HeadSHA, State: gitseat.PullStateOpen}}, nil
Head: p.HeadSHA, State: gitseat.PullStateOpen}, Merging: &gitseat.Merging{Mergeable: true, Protected: true,
Required: []string{"mesh/check"}, Writes: true}}, nil
}
}
return gitseat.PullAnswer{Error: "no such pull request"}, nil
@@ -371,16 +372,6 @@ func (r *runtime) answer(m rpcMessage) {
// Green: mesh/check a success at the head asked, as the seat answers.
commit, _ := a.Body["commit"].(string)
return gitseat.StatusesAnswer{Commit: commit, Statuses: []gitseat.Status{{Context: "mesh/check", State: "success"}}}, nil
case mergeStateTool:
// Green: mesh/check a success at the head asked, as the gitea module answers.
sha, _ := a.Body["sha"].(string)
if sha == "" {
sha = "0000000"
}
return toolAnswer(map[string]any{"commit": sha, "statuses": []any{
map[string]any{"context": "mesh/check", "state": "success"}},
"merge": map[string]any{"branch": "main", "protected": true, "required": []string{"mesh/check"},
"blocking": []any{}, "mergeable": true, "says": "main requires mesh/check"}}), nil
case "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status", "gitea.gitea_merge_pull_request",
"gitea.gitea_add_comment":
return toolAnswer(map[string]any{}), nil
@@ -20,10 +20,10 @@ func TestTheGateReadsAndHearsTheForgeThroughTheSeatGit(t *testing.T) {
"seat:git.open-pulls": gitseat.OpenPullsAnswer{Pulls: []gitseat.OpenPull{{Pull: pull, Required: []string{"mesh/check"},
Checks: map[string]gitseat.Status{"mesh/check": {Context: "mesh/check", State: "pending"}}}}},
"seat:git.contains": gitseat.ContainsAnswer{Contains: true},
"seat:git.pull": gitseat.PullAnswer{Pull: &pull},
"seat:git.pull": gitseat.PullAnswer{Pull: &pull, Merging: &gitseat.Merging{Mergeable: true, Protected: true,
Required: []string{"mesh/check"}, Writes: true}},
"seat:git.statuses": gitseat.StatusesAnswer{Commit: head, Combined: "pending",
Statuses: []gitseat.Status{{Context: "mesh/check", State: "pending", By: "mesh"}}},
mergeStateTool: map[string]any{"commit": head, "merge": map[string]any{"branch": "main", "mergeable": true}},
}
for _, c := range []struct {
name string
@@ -48,10 +48,23 @@ func TestTheGateReadsAndHearsTheForgeThroughTheSeatGit(t *testing.T) {
if err != nil {
return nil, err
}
return []any{c.Commit, *c.Pull, c.Statuses, *c.Merge.Mergeable}, nil
p := *c.Pull
conflict := *p.Mergeable
p.Mergeable = nil
return []any{c.Commit, p, c.Statuses, conflict, *c.Merge.Mergeable, c.Merge.Blocking[0].Context + " " +
c.Merge.Blocking[0].State, c.Merge.Says, *c.Merge.Writes}, nil
}, want: []any{head, ForgePull{Number: 7, State: "closed", Merged: true, Base: "main", Head: "feat/x", HeadSHA: head,
MergeCommit: pull.MergeCommit, HTMLURL: pull.URL}, []ForgeStatus{{Context: "mesh/check", State: "pending", Creator: "mesh"}}, true},
asked: []string{"seat:git.pull", "seat:git.statuses", mergeStateTool}},
MergeCommit: pull.MergeCommit, HTMLURL: pull.URL}, []ForgeStatus{{Context: "mesh/check", State: "pending", Creator: "mesh"}},
true, false, "mesh/check pending", "main requires mesh/check", true},
asked: []string{"seat:git.pull", "seat:git.statuses"}},
// What the forge would merge now comes from the pull answer's merging alone: none for a commit no pull request names.
{name: "no merge state for a commit alone", read: func(f seatForge) (any, error) {
c, err := f.Statuses("novox", "app", 0, head, "main")
if err != nil {
return nil, err
}
return []any{c.Merge.Mergeable == nil, c.Merge.Writes == nil}, nil
}, want: []any{true, true}, asked: []string{"seat:git.statuses"}},
{name: "a pull request not read", failed: "seat:git.pull",
read: func(f seatForge) (any, error) { return f.Statuses("novox", "app", 7, "", "") },
asked: []string{"seat:git.pull"}},
@@ -115,7 +115,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
// merge state through the one statuses tool the seat does not carry yet (seatForge.mergeState); the forge's writes are the
// bridge's (ADR 0437 §8). It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks
// (ADR 0437 §6).
want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", mergeStateTool, "seat:mesh-hand-acts.hand-act"}
want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", "seat:mesh-hand-acts.hand-act"}
if !reflect.DeepEqual(m.Uses, []string{"operator-channel"}) {
t.Fatalf("uses %v: operator-channel, for its conditions alone", m.Uses)
}
@@ -163,11 +163,13 @@ func gitAsk[A any](f seatForge, verb string, in any, errOf func(*A) string) (*A,
}
// Statuses reads a pull request by its number through `pull` (its head when no commit is named) and the commit's statuses
// through `statuses`. What the forge would merge now (the base's protection, its conflict word, whether the merging
// account writes) is read by mergeState.
// through `statuses`. What the forge would merge now (the base's protection and its required contexts, the forge's
// conflict word, whether the merging account writes) is the pull answer's `merging` (mergeState); a commit no pull
// request names has none.
func (f seatForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
repository := owner + "/" + repo
c := &ForgeChecks{Commit: commit}
var merging *gitseat.Merging
if number > 0 {
a, err := gitAsk(f, gitseat.VerbPull, gitseat.PullInput{Repository: repository, Number: int64(number)},
func(a *gitseat.PullAnswer) string { return a.Error })
@@ -177,7 +179,7 @@ func (f seatForge) Statuses(owner, repo string, number int, commit, base string)
if a.Pull == nil {
return nil, fmt.Errorf("the seat git's pull named no pull request for %s#%d", repository, number)
}
c.Pull = forgePullOf(*a.Pull)
c.Pull, merging = forgePullOf(*a.Pull), a.Merging
if c.Commit == "" {
c.Commit = a.Pull.Head
}
@@ -197,50 +199,58 @@ func (f seatForge) Statuses(owner, repo string, number int, commit, base string)
for _, st := range s.Statuses {
c.Statuses = append(c.Statuses, forgeStatusOf(st))
}
merge, mergeable, err := f.mergeState(owner, repo, number, c.Commit, base)
if err != nil {
return nil, err
}
c.Merge = merge
if c.Pull != nil {
c.Pull.Mergeable = mergeable
c.Merge = mergeState(repository, base, merging, c.Statuses)
if c.Pull != nil && merging != nil {
mergeable := merging.Mergeable
c.Pull.Mergeable = &mergeable
}
return c, nil
}
// mergeState is what the forge would merge now: the base's protection and its required statuses, the forge's own word on
// a conflict, and whether the merging account writes the repository. The seat `git` names none of them yet (dev-G-record's
// finding on W11.18): until it does, they are read through the forge holder's statuses tool, the one read left there.
func (f seatForge) mergeState(owner, repo string, number int, commit, base string) (ForgeMerge, *bool, error) {
args := map[string]any{"owner": owner, "repo": repo, "sha": commit}
if number > 0 {
args["number"] = number
// mergeState is what the forge would merge now, from the seat git's `merging` (issues 1275, 1945; ADR 0441 §1): the base's
// protection and its required contexts, each required context not a success at the commit blocking, and whether the
// merging account writes. Without `merging` (a commit no pull request names, or a pull request merged or closed) whether
// it merges is not known.
func mergeState(repository, base string, m *gitseat.Merging, statuses []ForgeStatus) ForgeMerge {
out := ForgeMerge{Branch: base, Required: []string{}}
if m == nil {
out.Says = "the seat git says what merges only for an open pull request"
return out
}
if base != "" {
args["base"] = base
out.Protected, out.Required = m.Protected, append([]string{}, m.Required...)
newest := map[string]string{}
for _, s := range statuses {
if _, seen := newest[s.Context]; !seen {
newest[s.Context] = s.State
}
}
raw, err := f.ask(mergeStateTool, args)
if err != nil {
return ForgeMerge{}, nil, err
for _, context := range out.Required {
if state := newest[context]; state != "success" {
if state == "" {
state = "missing"
}
out.Blocking = append(out.Blocking, struct {
Context string `json:"context"`
State string `json:"state"`
}{context, state})
}
}
answer, output, ok := answerOf(raw)
if !ok {
return ForgeMerge{}, nil, fmt.Errorf("the forge refused its merge state: %s", lastLine(output))
mergeable := len(out.Blocking) == 0
out.Mergeable = &mergeable
switch {
case len(out.Required) == 0:
out.Says = base + " requires no status"
default:
out.Says = base + " requires " + strings.Join(out.Required, ", ")
}
var c ForgeChecks
if err := json.Unmarshal(answer, &c); err != nil || c.Commit == "" {
return ForgeMerge{}, nil, fmt.Errorf("the forge's merge state is not readable: %s", clip(string(answer), 200))
writes := m.Writes
out.Writes = &writes
if !writes {
out.CannotWrite = "the forge's merging account may not write " + repository + ": its statuses and its merge are refused"
}
var mergeable *bool
if c.Pull != nil {
mergeable = c.Pull.Mergeable
}
return c.Merge, mergeable, nil
return out
}
// mergeStateTool is the one forge read the seat `git` does not carry yet (see mergeState).
const mergeStateTool = "gitea.gitea_commit_statuses"
// OpenPulls reads every open pull request through `open-pulls`.
func (f seatForge) OpenPulls() ([]OpenPull, error) {
a, err := gitAsk(f, gitseat.VerbOpenPulls, gitseat.OpenPullsInput{}, func(a *gitseat.OpenPullsAnswer) string { return a.Error })
-1
View File
@@ -257,7 +257,6 @@
"seat:git.statuses",
"seat:git.open-pulls",
"seat:git.contains",
"gitea.gitea_commit_statuses",
"seat:mesh-hand-acts.hand-act"
],
"state": [