Files
rfc-app/docs/DEV.md
T
Ben Stull a2bf89e90b Slice 3: the PR flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:37:54 -07:00

310 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build notes
The slicing plan for the v1 build, the current state of the codebase,
and the next slice's brief.
## The slicing plan
Eight slices carry §§115 of [`SPEC.md`](../SPEC.md) end-to-end. The
build does not extend the spec; spec corrections during the build are
rare and surgical and live in the appropriate numbered section per
§19.3's working agreement.
1. **Repository scaffolding + propose-to-super-draft vertical.** The
chokepoint that every Git operation flows through (§1 bot wrapper),
the §4 cache machinery (webhook + reconciler), the §5 schema, Gitea
OAuth + user provisioning, the minimal §7 catalog, and one
end-to-end vertical: propose → idea PR → merge → super-draft view.
2. **The active-RFC view per §8 in full.** Editor, branch creation,
per-branch chat with AI participation (the §18 `<change>` protocol),
the change-card panel, accept/decline/edit, manual-edit flushes,
sub-threads, flags, DiffView.
3. **The PR flow per §10.** Open, review surface (diff + compressed
chat), the §10.3 seen-cursor, §10.4 review threads, merge,
post-merge, §10.9 conflict resolution.
4. **Super-draft body editing per §9.5 + §9.6.** Meta-repo edit
branches as the unit of work; everything from §8 inherits.
5. **Graduation per §13.** The dialog, the five-step transactional
sequence, rollback, the pre-graduation history affordance.
6. **Notifications per §15.** Last, because every other surface
produces signals the inbox receives — notification correctness
depends on the producers being in place first.
7. **The §14 chrome.** Landing page polish, the `/philosophy` route,
the persistent About link.
8. **Hardening.** End-to-end tests, dev/prod deployment shape,
the §12 30/90 branch-hygiene timers.
## State of the codebase
### Slice 1 — shipped
The repository scaffolding (`backend/`, `frontend/`, `scripts/`,
`docs/`), the §5 schema as numbered migrations under
`backend/migrations/`, the §1 bot wrapper (`app/bot.py`) that is the
single chokepoint every Git write flows through, Gitea OAuth and the
§6.1 user-provisioning row in `users`, the §4.1 webhook receiver and
the §4.1 periodic reconciler (both writing to the cache; user actions
never do), the §7 left pane (catalog list, search, sort, state-filter
chips, pending-ideas disclosure), and one end-to-end vertical: propose
→ idea PR opens → owner merges → super-draft appears in the catalog →
super-draft view renders the body.
### Slice 2 — shipped
The §8 active-RFC view in full. The bot wrapper grew per-RFC-repo
write operations — branch cut from main, accept-change commit with
the structured `original`/`proposed`/`reason` body and trailers,
manual-edit flush, and a `ensure_rfc_repo_seed` seam Slice 5's
graduation will eventually replace. The §4 cache now mirrors per-RFC
repos via a new `refresh_rfc_repo` path; the webhook receiver
dispatches on `repository.full_name` so per-RFC events refresh just
that repo, and the reconciler sweeps every active entry. The §18
carryovers landed as `backend/app/providers.py` (the multi-provider
abstraction, unchanged from the prototype) and `backend/app/chat.py`
(an adapter that runs the provider's streaming interface against
`thread_messages` rows, parses `<change>` blocks, and materializes
`changes` rows per §8.14). The §17 endpoints owned by Slice 2 — the
`branches/<branch>/*` and `threads/<thread_id>/*` families — live in
`backend/app/api_branches.py`, mounted alongside Slice 1's routes via
`api.make_router`. On the frontend, `RFCView.jsx` was rebuilt as the
§8 three-column surface; `Editor.jsx`, `ChatPanel.jsx`,
`ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`,
`DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` were lifted
from the prototype and adapted to the canonical `threads` /
`thread_messages` / `changes` shape rather than the prototype's
global session_id. The §18 carryovers explicitly preserved: SSE
streaming with base64-encoded chunks, Tiptap + ProseMirror plugin for
the paragraph-margin gutter accent, the prompt-bar selection-quote
machinery, the model picker.
The §17 endpoints exercised so far:
| Method | Path | § |
| ------ | -------------------------------------- | ------- |
| GET | `/api/auth/me` | §6 |
| GET | `/api/rfcs` | §7, §17 |
| GET | `/api/rfcs/{slug}` | §17 |
| GET | `/api/proposals` | §17 |
| GET | `/api/proposals/{pr_number}` | §17 |
| POST | `/api/rfcs/propose` | §9.1 |
| POST | `/api/proposals/{pr_number}/merge` | §9.3 |
| POST | `/api/proposals/{pr_number}/decline` | §9.3 |
| POST | `/api/proposals/{pr_number}/withdraw` | §9.3 |
| POST | `/api/webhooks/gitea` | §4.1 |
| GET | `/auth/login` / `/auth/callback` / `/auth/logout` | §18 |
| GET | `/api/models` | §18 |
| GET | `/api/rfcs/{slug}/main` | §8.1, §8.2, §17 |
| GET | `/api/rfcs/{slug}/branches/{branch}` | §8.4, §17 |
| POST | `/api/rfcs/{slug}/branches/main/promote-to-branch` | §8.14, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/accept` | §8.9, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/decline` | §8.9, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/reask` | §8.11, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/manual-flush` | §8.11, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/visibility` | §11.1, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/grants` | §6.4, §17 |
| DELETE | `/api/rfcs/{slug}/branches/{branch}/grants/{login}` | §6.4 |
| GET | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §17 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §8.13 |
| GET | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/resolve` | §8.12 |
| POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/chat` | §18 |
Slice 2 ships covered by `backend/tests/test_rfc_view_vertical.py`
the FakeGitea simulator from Slice 1 grew per-RFC-repo support (PUT
contents, POST `orgs/{org}/repos`, `seed_rfc_repo`), and a new test
file walks the §8 vertical end-to-end: main-view read, promote-to-
branch, accept (with and without edit-before-accept), decline, manual
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
build, so they were scoped out of this slice without altering the
spec:
- **Super-draft body editing on the meta repo (§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" gesture to cut against the
meta repo. The Slice 2 RFCView renders a placeholder for
super-draft entries pointing at Slice 4.
- **The §10.4 review threads on PRs.** `thread_kind='review'` is in
the schema and the threads endpoints honor it generically, but the
PR-page surface where review threads anchor to diff hunks lands
with Slice 3.
- **DiffView's full reconstruction from `changes` history.** Slice 2
renders the editor's current HTML (which carries the
session-local tracked-change markup from the accepts that happened
in this session) into DiffView; rebuilding the full accepted-change
markup from `changes` for a returning contributor needs a render
pipeline DiffView doesn't yet own. The current behavior matches
§8.10's "session-local" framing exactly; the §19.2 "persistent
accepted-change markup" topic is the durable extension when
evidence demands it.
- **The §10.6 PR-side commit / chat reconciliation.** Manual-edit
flushes drop a system-author message into branch chat per §10.6
in Slice 2, but the PR-side seen-cursor that uses the marker
ships with Slice 3.
- **Branch-name path conversion for slashes.** The auto-generated
branch name in Slice 2 is `<login>-draft-<hex>` (no slash) so the
FastAPI `{branch}` path segment matches without `{branch:path}`.
Users can still rename to a slashed name, but the routes will
404 on read; the proper fix is `{branch:path}` everywhere, which
lands cleanly when Slice 3 makes the same change to the PR routes
(PR numbers don't have this problem, but resolving the routing
shape once across both surfaces is the right hop).
## Environment notes
- **Python 3.13.** Earlier 3.11+ should also work; 3.13 is what the
build session ran on.
- **Node 20+** for the frontend.
- **Local Gitea on port 3000.** Anything that exposes the Gitea v1
REST API works. If you tunnel Gitea elsewhere (e.g. a container,
a Codespace), re-run `scripts/seed_meta_repo.py` so the webhook
re-registers against the right `APP_URL`.
## Conventions
- **Bot writes only via `app/bot.py`.** If a module wants to call
`app/gitea.py`'s write methods directly, the spec is right and
the module is wrong — the wrapper is the chokepoint that makes
the §6.5 `On-behalf-of:` trailer and the §6 authorization both
consistent.
- **Cache writes only from `app/cache.py`.** User actions trigger
Git operations via the bot; the cache learns about them when the
webhook arrives (or the next reconciler sweep), and never before.
This invariant is what makes §4's "Git is truth" claim hold
operationally.
- **Spec corrections during the build are rare and surgical.** When
running code reveals the spec was wrong at a structural level (per
§19.3's working agreement), the correction lands in the appropriate
numbered section with a brief note explaining what running code
revealed. Spec extensions during the build are not in scope —
they accumulate in §19.2.
- **§16 stays deferred.** Body full-text search, per-RFC model
picker, funder role, persistent accepted-change markup, slug
renames — these are not shipped in any slice. They earn their own
topic sessions when use surfaces evidence they matter.
## Next slice
**Slice 4: super-draft body editing per §9.5 + §9.6.**
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).
What Slice 4 owns specifically:
- §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 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
extend the spec beyond what the slice requires.