Slice 3: the PR flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-24 12:37:54 -07:00
parent 33d9d7a482
commit a2bf89e90b
15 changed files with 2928 additions and 141 deletions
+106 -30
View File
@@ -119,6 +119,73 @@ flush + system message, flag creation, visibility flip, anonymous
read-but-no-contribute, stale-change refusal, and the chat-streaming
path with a fake provider injected.
### Slice 3 — shipped
The §10 PR flow in full. The bot wrapper grew per-RFC-repo PR
operations — `open_branch_pr` (with the §10.9 `Supersedes:` trailer
hook), `merge_branch_pr` (no-fast-forward via Gitea's `style='merge'`,
the `On-behalf-of:` trailer carrying the merging user per §6.5),
`withdraw_branch_pr`, `cut_resolution_branch`, and
`commit_replay_change` for the §10.9 per-accept replay onto fresh
main. The §4 cache learned about per-RFC PRs via the existing
`refresh_rfc_repo` sweep, plus a `_parse_supersedes` pass that bumps
an original PR's state to closed and records the supersession the
moment the resolution PR's merge arrives — whether via webhook or
the reconciler. The §17 endpoints owned by Slice 3 — the
`branches/<branch>/{pr-draft,open-pr}` and the `prs/<n>/*` family —
live in `backend/app/api_prs.py`, mounted alongside Slices 1 and 2's
routes via `api.make_router`. The migration in `007_pr_flow.sql`
adds `superseded_by_pr_number` and `merge_commit_sha` columns to
`cached_prs` plus the `pr_resolution_branches` join table that
records resolution-branch parentage so the cache can supersede the
original on the resolution PR's merge.
On the frontend, the `Open PR` affordance landed on `RFCView.jsx`'s
branch view (gated on the branch having commits ahead of main and no
already-open PR), opening a new `PRModal.jsx` that fetches the AI
draft via `/pr-draft`, lets the contributor edit, and surfaces the
§11.3 universal-public confirmation inline when the source branch is
private. The `PRView.jsx` sibling to `RFCView.jsx` is mounted at
`/rfc/:slug/pr/:prNumber` and renders the §10.3 three-column shape:
catalog left (App chrome), a unified/split diff in the center
computed from main and branch RFC.md bodies, and a compressed
conversation surface on the right that interleaves chat / flag /
review threads with visual distinction per §10.4. The per-user
seen-cursor advances on every visit; new commits and new messages
since the cursor surface with an accent. The merge button is
arbiter-gated per §6.3; withdraw is contributor-or-arbiter per §10.8;
the §10.9 `Start resolution branch` affordance fires from the
conflict banner when the live Gitea pull reports the PR as
unmergeable, and the new resolution branch opens in the §8 editor for
the contributor to re-anchor stale changes before opening the
resolution PR.
The §17 endpoints exercised in Slice 3:
| Method | Path | § |
| ------ | ----------------------------------------------- | ------- |
| POST | `/api/rfcs/{slug}/branches/{branch}/pr-draft` | §10.2 |
| POST | `/api/rfcs/{slug}/branches/{branch}/open-pr` | §10.1 |
| GET | `/api/rfcs/{slug}/prs/{n}` | §10.3 |
| POST | `/api/rfcs/{slug}/prs/{n}/seen` | §10.3 |
| POST | `/api/rfcs/{slug}/prs/{n}/review` | §10.4 |
| POST | `/api/rfcs/{slug}/prs/{n}/merge` | §10.5 |
| POST | `/api/rfcs/{slug}/prs/{n}/withdraw` | §10.8 |
| POST | `/api/rfcs/{slug}/prs/{n}/description` | §10.2 |
| POST | `/api/rfcs/{slug}/prs/{n}/resolution-branch` | §10.9 |
Slice 3 ships covered by `backend/tests/test_pr_flow_vertical.py`
nine integration tests against an extended FakeGitea that grew PR
mergeability via base-snapshot tracking, no-fast-forward merge
behavior, and a `mergeable` field on PR responses. The tests cover
opening (with the §11.3 visibility flip and the §10.9 one-PR-per-
branch refusal), the AI draft, the three-column payload shape,
seen-cursor advance with stale-tab protection, review-thread
posting, arbiter-only merge, contributor withdraw with the
`withdrawn` state distinct from generic `closed`, anonymous read
of a public PR, and the full §10.9 conflict-replay path including
the auto-close of the original PR on the resolution PR's merge.
### What's deferred from Slice 2
These were in the §8 spec but lean on infrastructure later slices
@@ -192,41 +259,50 @@ spec:
## Next slice
**Slice 3: the PR flow per §10.**
**Slice 4: super-draft body editing per §9.5 + §9.6.**
§8 settled the within-branch surface; §10 settles the bridge between
a branch and main. The work covers the `Open PR` affordance from
§10.1 (with the §11.3 universal-public confirmation when the branch
is private), the §10.2 AI-drafted creation modal (title +
description from the diff plus the branch chat), the §10.3 review
page (three-column, diff in the center, compressed conversation
right, per-user seen-cursor accenting new hunks and new messages),
the §10.4 `thread_kind='review'` threads anchored to diff hunks
inline in branch chat, §10.5 merge (no-fast-forward, preserving the
per-acceptance commits), §10.6 update-after-open (commits and chat
arriving on the open PR, the manual-flush system message that
already lands per Slice 2), §10.7 post-merge (`Merged` banner,
chat read-only, 90-day deletion timer starts), §10.8 withdraw, and
§10.9 conflict-replay with the resolution-branch path. The shared
seen-cursor mechanism in §15.7 (the `pr_seen` and
`branch_chat_seen` cursors are in the schema already; Slice 3 wires
the advance-on-view reconciler).
The §8 within-branch surface and the §10 bridge to main now ship for
active RFCs; the same mechanics still need to reach super-draft
entries on the meta repo. Slice 4's unit of work is the meta-repo
edit branch — `edit/<slug>/<auto-name>` per §9.5 — and the
structural claim is that almost everything from §8 falls out
unchanged once `<slug>` resolves to a super-draft entry and
`<branch>` names a meta-repo branch rather than a per-RFC-repo
branch (see the §5 super-draft scoping note).
The carryovers Slice 3 inherits — none new from the prototype; the
prototype's `PRModal.jsx` had a one-shot PR-creation flow that the
spec's §10 expanded considerably. The `backend/app/bot.py` operations
Slice 3 needs are: `open_pr`, `merge_pr` (style='merge' to preserve
the per-accepted-change commit nodes per §10.5), `close_pr` (for
withdraw), and the resolution-branch replay sequence from §10.9 —
which is structurally a `cut_branch_from_main` plus a series of
`commit_accepted_change` calls plus an `open_pr`.
What Slice 4 owns specifically:
The frontend needs a `PRView.jsx` sibling to `RFCView.jsx` that
inherits the §8.1 three-column shape but renders the diff instead
of the editor. The route is `/rfc/<slug>/prs/<n>`.
- §9.5's `Start Contributing` on a super-draft cutting an
`edit/<slug>/<auto-name>` branch on the meta repo via the bot,
re-anchoring pending `changes` rows from `main` to the new branch
the way `promote-to-branch` does for active RFCs.
- §9.6's chat-and-threads surface scoped to the super-draft and to
edit branches, sharing the §5 `threads`/`thread_messages` shape.
- §9.7's visibility and contribute grants on edit branches — the
same `branch_visibility` / `branch_contribute_grants` machinery
that Slice 2 wired, now keyed on the meta repo.
- The metadata pane from §9.5 — title and tag edits as small
meta-repo PRs via `POST /api/rfcs/{slug}/metadata`. Slug renames
remain deferred per §9.5 / §19.2.
- The §17 routing collapse the spec calls for: the
`branches/<branch>/...` endpoint family already exists; Slice 4's
job is the dispatch in `api_branches.py` that recognizes a
super-draft slug and routes to the meta repo on every read and
write. `RFCView.jsx`'s super-draft placeholder is replaced by the
full editor surface.
What Slice 4 does NOT own: the §10 PR flow against the meta repo's
super-draft edits is structurally identical to the active-RFC PR
flow Slice 3 just shipped, and falls out from the same dispatch.
The graduation flow from §13 stays deferred to Slice 5.
The carryovers Slice 4 inherits — none new from the prototype;
every §8 / §10 surface already exists. The work is dispatch glue
plus a small number of routes that need the meta-repo path
(`branches/edit/<slug>/<auto-name>` cuts).
The next build session should read `SPEC.md`, `README.md`, and
`docs/DEV.md` and pick up Slice 3 cleanly without re-briefing. The
`docs/DEV.md` and pick up Slice 4 cleanly without re-briefing. The
working agreement in §19.3 continues to apply: implement the slice,
correct the spec only where running code reveals it was wrong at a
structural level, accumulate new candidate topics in §19.2, do not