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
+118 -96
View File
@@ -2405,108 +2405,87 @@ surface. With Topic 13 folded in, the structural surface is
complete. What follows is no longer "topics that block specifying
v1" but "topics to address during or shortly after the v1 build."
### 19.1 Next slice: the PR flow
### 19.1 Next slice: super-draft body editing
Slice 2 of the build has landed. The §8 active-RFC view is wired
end-to-end against the local Gitea: the three-column shape (§8.1)
inherits the §7 catalog on the left, hosts a Tiptap editor in the
center with a breadcrumb dropdown listing main + open branches +
open PRs, and surfaces per-branch chat plus the change-card panel
on the right. Opening an active RFC lands on `main` in discuss mode
per §8.2; `Start Contributing` on main calls the §17
promote-to-branch endpoint to cut a new branch via the bot and
re-anchors any pending `main`-scoped `changes` rows to it (§8.14).
On a non-main branch the §8.3 discuss-vs-contribute toggle flips
the editor between read-only and edit-enabled. The §18 carryovers
landed in `backend/app/providers.py` and `backend/app/chat.py` and
on the frontend as `Editor.jsx`, `ChatPanel.jsx`, `ChangePanel.jsx`,
`PromptBar.jsx`, `SelectionTooltip.jsx`, `DiffView.jsx`, and
`ModelPicker.jsx`. AI chat parses `<change>` blocks per §18 into
`changes` rows with `state='pending'` per §8.14; accept runs the
bot's per-accepted-change commit (§8.6) with the structured body
and `Change-Id`, `Source-Message-Id`, and `On-behalf-of:` trailers;
decline persists the row as evidence per §8.9; edit-before-accept
preserves the AI's original text under an `AI proposed:` section
of the commit body per §8.9. Manual edits flush as one commit per
window with a system-author chat message landing per §10.6. The
§8.10 tracked-change markup is session-local in the editor; DiffView
is the read-only render of the same accepted changes. The §8.11
stale-change machinery sets `changes.stale_since` when a manual
edit changes the document such that a pending AI proposal's
`original` no longer locates; the re-ask and force-apply paths are
wired. The §8.12 range threads (via the selection tooltip) and the
§8.13 flag threads (via the selection tooltip's flag tab) materialize
as `threads` rows scoped to the branch; the chat feed renders them
inline with the whole-doc default thread. The §11.1 visibility and
§6.4 contribute grants are wired with the branch-creator /
arbiter / admin set per §11.1, §11.2, §6.3. The §4 cache grew a
`refresh_rfc_repo` path that the webhook dispatches per
`repository.full_name` and the reconciler sweeps for every active
entry. The vertical is covered by `backend/tests/test_rfc_view_vertical.py`
— eleven integration tests against an extended FakeGitea that
supports per-RFC repos.
Slice 3 of the build has landed. The §10 PR flow is wired
end-to-end against the local Gitea the §10.1 `Open PR`
affordance on a branch with the §11.3 universal-public flip when
the source branch is private, the §10.2 AI-drafted modal pulling
title and description from the diff plus the branch chat (with a
deterministic stub when no provider is configured), and the §10.3
PR review page mounted at `/rfc/<slug>/pr/<n>` that inherits the
§8.1 three-column shape and renders a unified/split diff in the
center against a compressed conversation on the right that
interleaves chat, flag, and review threads with visual
distinction. The §10.3 per-user seen-cursor advances on every
visit and accents new commits and new messages on the next; stale
tabs cannot roll the cursor backward. §10.4 review threads
materialize as `thread_kind='review'` `anchor_kind='range'` rows
on the branch chat, surfaced inline with the AI conversation but
distinguished by header badge. §10.5 merge runs the bot through
Gitea's `style='merge'` no-fast-forward path with an
`On-behalf-of:` trailer on the merge commit, preserving the §8.6
per-acceptance commits as reachable nodes in main's history.
§10.6 update-after-open falls out of Slice 2's existing
per-accept-and-per-flush push paths plus the new diff re-render on
every PR view. §10.7 post-merge renders the PR read-only with a
`Merged` banner; §10.8 withdraw collapses the PR to read-only with
a `Withdrawn` banner, distinguishing the user gesture from a
generic Gitea close via the audit log. §10.9 conflict-replay
surfaces a `Start resolution branch` affordance from the conflict
banner when Gitea reports the PR as unmergeable, cuts a fresh
branch off main's tip via the bot, replays the original branch's
accepted AI changes onto the resolution branch — applying each one
whose `original` text still locates exactly once, surfacing the
rest as stale-pending changes the contributor can re-anchor — and
opens a new PR whose `Supersedes:` trailer the cache parses on the
resolution PR's merge to auto-close the original.
Several §8 / §10 affordances were deferred from Slice 2 to later
slices — they're not new candidate topics, only delivery sequencing:
The §10 endpoints live in `backend/app/api_prs.py`, mounted
alongside the Slice 1 and 2 routers. The bot grew
`open_branch_pr`, `merge_branch_pr`, `withdraw_branch_pr`,
`cut_resolution_branch`, and `commit_replay_change`. The §5
schema grew `cached_prs.superseded_by_pr_number`,
`cached_prs.merge_commit_sha`, and a `pr_resolution_branches` join
table that records resolution-branch parentage. On the frontend,
the `Open PR` button landed on `RFCView.jsx`'s branch view,
opening `PRModal.jsx`; `PRView.jsx` is the §10.3 page in full.
- **Super-draft body editing on meta-repo edit branches (§9.5).**
The `branches/<branch>` machinery is structurally general enough
that meta-repo edit branches fall out of it once Slice 4 wires
the super-draft view's "Start Contributing" to cut against the
meta repo. The Slice 2 RFCView renders a placeholder for
super-draft entries pointing at Slice 4.
- **PR-anchored review threads (§10.4).** `thread_kind='review'` is
in the §5 schema and the threads endpoints honor it generically,
but the PR-page surface that anchors review threads to diff
hunks lands with Slice 3.
- **DiffView's full reconstruction from `changes` history.** Slice 2
's DiffView renders the editor's current HTML, which carries the
session-local tracked-change markup from accepts done in the
current session. Rebuilding the markup for accepted changes
earlier in branch history is the §19.2 "persistent
accepted-change markup" topic; the §8.10 framing already commits
the markup to session-local scope and points returning
contributors at DiffView, which is the durable artifact.
- **The §10.6 PR-side seen-cursor reconciliation.** Manual-edit
flushes drop a system-author message per §10.6 in Slice 2, but
the per-PR seen-cursor that uses the marker ships with Slice 3.
Slice 3 ships covered by `backend/tests/test_pr_flow_vertical.py`
— nine integration tests against an extended FakeGitea that grew
PR mergeability tracking via per-branch base snapshots,
no-fast-forward merge behavior, and a `mergeable` field on PR
responses. The tests cover opening with the §11.3 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.
**Slice 3 is the PR flow per §10 in full.** Open a PR via the
§10.1 affordance on a branch (with the §11.3 universal-public
confirmation when the branch is private); the §10.2 AI-drafted
creation modal pulls title and description from the diff plus the
branch chat. The §10.3 PR review page inherits the §8.1
three-column shape — catalog left, diff (unified or split) in the
center, the compressed conversation plus the inline review-comment
surface (§10.4) on the right. The §10.3 per-user seen-cursor
mechanism accents new hunks and new conversation messages on the
next visit. The §10.4 review comments materialize as
`thread_kind='review'`, `anchor_kind='range'` threads anchored to
the post-PR document state, surfaced inline with the AI chat
visually distinguished. §10.5 merge writes a no-fast-forward merge
commit preserving the per-accepted-change commit nodes from §8.6
as individual reachable commits in main's history. §10.6 update-
after-open re-renders the diff as new commits arrive (which they
already do — Slice 2's manual-flush and accept-change paths both
push immediately). §10.7 post-merge renders the PR read-only with
a `Merged` banner and starts §12's 90-day deletion timer. §10.8
withdraw closes the PR with the same read-only treatment. §10.9
conflict-replay cuts a fresh resolution branch off main's tip,
replays the source branch's diff (running the AI participant
against unambiguous conflicts and surfacing the rest to the
contributor), and opens a new PR with the original auto-closing on
merge.
The carryover assets Slice 3 inherits: none new from the prototype
beyond what Slice 2 already lifted. The prototype's `PRModal.jsx`
was a one-shot submission flow; §10's PR creation modal is its
descendant but the spec broadened the surface considerably. The
seen-cursor advances are pure schema work — `pr_seen` and
`branch_chat_seen` are in the §5 schema; Slice 3 wires the
advance-on-view reconciler from §15.7.
**Slice 4 is super-draft body editing per §9.5 + §9.6.** The
unit of work is the meta-repo edit branch — `edit/<slug>/<auto-
name>` per §9.5 — and almost everything from §8 falls out
structurally unchanged once `<slug>` resolves to a super-draft
entry and `<branch>` names a meta-repo branch rather than a
per-RFC-repo branch, per the §5 super-draft scoping note and §17's
single-dispatch rule. The §9.5 `Start Contributing` gesture on a
super-draft cuts a meta-repo edit branch via the bot and
re-anchors pending main-scoped `changes` rows. The §9.6 chat-and-
threads surface inherits the existing `threads` /
`thread_messages` shape. The §9.7 visibility and contribute grants
on edit branches reuse the Slice 2 machinery, keyed on the meta
repo. The metadata pane from §9.5 lands as
`POST /api/rfcs/{slug}/metadata` — title and tag edits as small
meta-repo PRs via the bot. Slug renames remain deferred per §9.5
and the §19.2 candidate entry. The PR flow against meta-repo
edits is structurally identical to the active-RFC PR flow Slice 3
shipped and falls out from the same dispatch; the graduation flow
from §13 stays deferred to Slice 5.
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
@@ -2651,6 +2630,49 @@ binding.
"from a conversation on main" in the change panel; a small visual
treatment is the natural follow-on. Surfaced by §8.14's data path
going through Slice 2 for the first time.
- **The PR-page diff renderer.** Slice 3 ships a line-level
unified/split diff between branch and main RFC.md bodies,
computed client-side from the two strings via a small LCS pass.
Sufficient for the single-file v1 surface, but the §10.3
per-hunk seen-cursor accent — distinct from the file-level
accent Slice 3 wires — and the inline `<change>`-block
attribution from `changes.commit_sha` are the natural follow-on.
Earns a topic when a contributor's PR carries enough changes
that a reviewer wants to scope review to one hunk at a time.
Touches §10.3 (the per-hunk accent voice) and §10.4 (anchoring
review threads to specific hunks rather than free-text quotes).
- **The §10.2 modal's AI-drafted text when no provider is
configured.** Slice 3 falls back to a deterministic stub
(`Edits to <RFC title>` plus a character-count line) when the
app has no LLM provider. The fallback is functional but does
not produce spec-voice text. Per-RFC model availability (the
first §19.2 candidate, on the funder-role topic) will need to
settle the credential-delegation shape before this earns its
own topic; until then, the stub is the right shape for the
no-credential-available case.
- **§10.9 replay AI participation.** Slice 3 implements the
structural §10.9 path — fresh resolution branch off main, replay
the accepted changes whose `original` text still locates exactly
once, surface the rest as stale-pending changes on the
resolution branch — but does not yet invoke the AI participant
on the ambiguous conflicts to attempt a re-anchored proposal.
The contributor re-anchors manually for now. The "AI runs
against unambiguous conflicts" pass earns its own topic once
conflicts happen often enough to design against; the §19.2
"conflict-replay UX in detail" entry already names this.
- **PR title and description sync with Gitea.** Slice 3's
`POST /api/rfcs/.../prs/<n>/description` updates the cache row
but does not mirror the edit back to Gitea via the issues
endpoint. The PR page is the canonical surface for v1 and the
cache is its source of truth, so the divergence is fine within
the app — but anyone reading the PR directly on Gitea sees the
pre-edit text. A small follow-on that propagates the edit
through the bot wrapper closes the loop.
- **The §10.7 90-day deletion timer wiring.** Slice 3 lands the
PR-merged state and the read-only treatment but does not wire
the §12 hygiene timer that fires the deletion. Slice 8
("Hardening") owns the §12 30/90 timers as a whole; calling out
here so the dependency is explicit.
- **Body full-text search.** When the time comes.
Topic 13 (notifications) is settled and folded into §5 (the