From 0aa5d6637c1a554830d37ba0fce2ca0043dffbd5 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 22:18:20 +0100 Subject: [PATCH] C1: the gate and the forge hold no branch name; the trunk is the asked branch, a default is the repository's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §428, §507, A-93 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/gitea/test/protection-read.test.ts | 14 +++++++++++ modules/gitea/tools/index.ts | 14 +++++++---- .../cmd/mesh-delivery-gate/main.go | 4 ++-- .../cmd/mesh-delivery-gate/merges.go | 23 ++----------------- .../mesh-delivery-gate/sourcetools_test.go | 19 +++++++++++++++ .../cmd/mesh-delivery-gate/values.go | 8 ------- 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/modules/gitea/test/protection-read.test.ts b/modules/gitea/test/protection-read.test.ts index b774da455..68199b22a 100644 --- a/modules/gitea/test/protection-read.test.ts +++ b/modules/gitea/test/protection-read.test.ts @@ -17,12 +17,19 @@ const rules = [ { branch_name: "release", enable_push: false }, ]; const seen: string[] = []; +const opened: { base?: string }[] = []; const server = createServer((req, res) => { const path = (req.url ?? "").replace(/^\/api\/v1/, ""); const as = req.headers.authorization === adminBasic ? "admin" : "tools"; seen.push(`${req.method} ${path} as ${as}`); const send = (status: number, body: unknown) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(body)); }; + if (path === "/repos/novox/app") return send(200, { full_name: "novox/app", default_branch: "trunk" }); + if (req.method === "POST" && path === "/repos/novox/app/pulls") { + let raw = ""; + req.on("data", (c) => (raw += c)); + return req.on("end", () => { opened.push(JSON.parse(raw)); send(201, { number: 7, title: "t", state: "open", html_url: "u" }); }); + } if (path.includes("/branch_protections")) { if (as !== "admin") return send(403, { message: "user should be an owner or a collaborator with admin write of a repository" }); if (req.method === "GET") return send(200, rules); @@ -57,3 +64,10 @@ test("gitea_branch_protection_set still gets the forge's refusal, as the tools' ); assert.ok(!seen.some((s) => /^(PATCH|POST|PUT|DELETE) .*branch_protections.* as admin$/.test(s)), seen.join("\n")); }); + +// Design 47 G §428: no part's code holds a branch's name; a pull request opened with no base goes to the repository's default branch. +test("gitea_create_pull_request with no base opens against the repository's default branch, and a named base stays", async () => { + await tool("gitea_create_pull_request").run({ owner: "novox", repo: "app", title: "t", head: "feat/x" }); + await tool("gitea_create_pull_request").run({ owner: "novox", repo: "app", title: "t", head: "feat/x", base: "release" }); + assert.deepEqual(opened.map((o) => o.base), ["trunk", "release"]); +}); diff --git a/modules/gitea/tools/index.ts b/modules/gitea/tools/index.ts index 86ad0d0c9..2f0c105f5 100644 --- a/modules/gitea/tools/index.ts +++ b/modules/gitea/tools/index.ts @@ -93,6 +93,12 @@ export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): Too // A merge, a status, a comment, a delivery's view and its note are the seat git's acts (seat.ts), asked by the forge // bridge alone: no tool of the module's own does them (novox/hq design 47 *The seat git*). function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined): ToolDefinition[] { + // A branch no caller named is the repository's default branch, as the forge says it; no part's code holds one (§428). + const defaultBranchOf = async (owner: string, repo: string): Promise => { + const found = String((await gitea.getRepo(owner, repo)).default_branch ?? ""); + if (!found) throw new Error(`the forge names no default branch of ${owner}/${repo}: name the branch`); + return found; + }; return [ // ---- Repositories ---- { @@ -269,14 +275,14 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined title: { type: "string", description: "the PR title" }, body: { type: "string", description: "the PR body (markdown)" }, head: { type: "string", description: "the source branch" }, - base: { type: "string", description: "the target branch (default main)" }, + base: { type: "string", description: "the target branch (default: the repository's default branch)" }, }, run: async (args) => ({ pull: await gitea.createPullRequest(String(args.owner), String(args.repo), { title: String(args.title), body: args.body ? String(args.body) : undefined, head: String(args.head), - base: args.base ? String(args.base) : "main", + base: args.base ? String(args.base) : await defaultBranchOf(String(args.owner), String(args.repo)), }), }), }, @@ -679,7 +685,7 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined repo: { type: "string", description: "the repository name" }, number: { type: "number", description: "a pull request's number: its head is read, and its base's protection" }, sha: { type: "string", description: "a commit, or the start of one, instead of a pull request" }, - base: { type: "string", description: "with sha: the branch whose protection is read (default main)" }, + base: { type: "string", description: "with sha: the branch whose protection is read (default: the repository's default branch)" }, }, run: async (args) => { const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim(); @@ -702,7 +708,7 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined if (!ref) throw new Error("name a pull request's number or a commit"); const combined = await gitea.combinedStatus(owner, repo, ref); const statuses = newestByContext(combined.statuses); - base = base || "main"; + base = base || (await defaultBranchOf(owner, repo)); let rule: BranchProtection | null = null, unreadable: string | undefined; try { rule = await gitea.appliedProtection(owner, repo, base); diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go index 465482dea..60984b018 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -283,10 +283,10 @@ func tools(h *Holder, l *listening) []stdio.Tool { {Name: "delivery_status", Description: "Whether this owner works: how many delivery records are open and ended, what is owed to the forge and not yet " + "answered, how many owed effects were tried since it started (done, failed and tried again, given up) and when the last " + - "was, whether its events arrive, the last refusals, and source.main as the gate's bucket source holds it.", + "was, whether its events arrive, the last refusals, and the source revision of every branch the gate's bucket source holds.", Run: func(map[string]any) (any, error) { out := h.Status(l.get()) - out["source-main"] = h.SourceMain() + out["source"] = h.SourceAll() return out, nil }}, } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go index dd42ac7e2..12d88bc9e 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go @@ -16,9 +16,6 @@ import ( // (switch.go, §613), never written; `source` is the gate's source revision per watched branch (§15, §291), served by the verb // `source` below. -// MergeBranch is the trunk branch each repository's merge branch names in a source key. -const MergeBranch = "main" - // changeAttempts is how often a lost compare-and-set is read and applied again before it is said. const changeAttempts = 10 @@ -155,7 +152,7 @@ func (kvSource) Branches() ([]string, error) { // SourceRevision is the seat's verb source (§690): given a branch, every repository that branch's source key names, its // trunk branch and head, the group that moved it last and its revision, or why it cannot be read; given none, `branches`, -// the branch of each source key the gate keeps. A key names each repository's merge branch only (MergeBranch). Outside the lock. +// the branch of each source key the gate keeps. The trunk of each repository is the branch asked, the key's own (§428, §507). Outside the lock. func (h *Holder) SourceRevision(branch string) seat.SourceAnswer { if h.Source == nil { return seat.SourceAnswer{Error: "no source bucket"} @@ -179,24 +176,8 @@ func (h *Holder) SourceRevision(branch string) seat.SourceAnswer { } out := seat.SourceAnswer{Branch: branch, Group: held.Group, Revision: int64(revision)} for repository, head := range held.Heads { - out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: MergeBranch, Head: head}) + out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: branch, Head: head}) } sort.Slice(out.Repositories, func(i, j int) bool { return out.Repositories[i].Repository < out.Repositories[j].Repository }) return out } - -// SourceMain is source.main as the bucket source holds it, read and never written: its group and each repository's trunk -// head, or why it cannot be read. Outside the lock. -func (h *Holder) SourceMain() map[string]any { - if h.Source == nil { - return map[string]any{"error": "no source bucket"} - } - held, _, err := h.Source.Read(MergeBranch) - switch { - case err != nil: - return map[string]any{"error": err.Error()} - case held == nil: - return map[string]any{"heads": map[string]string{}, "says": "source.main is not written yet"} - } - return map[string]any{"group": held.Group, "heads": held.Heads} -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go index 12204f773..2094ff7b5 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go @@ -84,3 +84,22 @@ func TestTheGateStatesItsPinsThenSaysPinsStated(t *testing.T) { t.Fatalf("%d %v %+v", n, err, events) } } + +// Design 47 G §428, §507: no part's code holds a branch's name; the trunk of each repository a source revision names is the +// branch whose key was read, and delivery_status says every branch's source, not one fixed branch's. +func TestTheSourceAnswerNamesTheBranchItWasAskedAndStatusSaysEveryBranch(t *testing.T) { + src := &branchSource{} + for _, b := range []string{"release", "feat"} { + _, _ = src.Set(b, func(*Source) (Source, bool) { + return Source{Group: "g-" + b, Heads: map[string]string{"novox/app": "c1"}}, true + }) + } + h := &Holder{Now: time.Now, Source: src} + got := h.SourceRevision("release") + if len(got.Repositories) != 1 || got.Repositories[0].Trunk != "release" { + t.Fatalf("the trunk is the asked branch: %+v", got.Repositories) + } + if all, ok := h.SourceAll()["branches"].([]seat.SourceAnswer); !ok || len(all) != 2 { + t.Fatalf("status says every branch's source: %+v", h.SourceAll()) + } +} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go index b3883cfc8..b1de5f8ce 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go @@ -8,14 +8,6 @@ import ( seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" ) -// The holders the gate reads or hears, by the names their modules go by (the gate's own constants, one per reader: the -// manifest's reads and consumes name them). -const ( - tellerModule = "mesh-delivery-teller" - viewModule = "mesh-delivery-view" - forgeBridgeModule = "mesh-delivery-forge-bridge" -) - // The values the gate keeps in the seat mesh-delivery's states. The seat's types are the layout and meshdeliveryclient // holds their rules and codecs; Source, Owed and Answer are used as they are, and the three whose seat form differs from the // form the gate works in (a pull request that holds its head, the router's ask as bytes, a turn's times as plain times)