The §17 routing-collapse rule lands in api_branches.py and api_prs.py — every branches/<branch>/... and prs/<n>/... route dispatches on the entry's state to pick the right Gitea repo, and the body extracted from the entry's frontmatter envelope is what the editor and the diff see. The bot grows open_metadata_pr; cache grows refresh_meta_branches. Two §17 routes added: start-edit-branch and metadata. The §9.4 super-draft view replaces RFCView.jsx's Slice 2 placeholder; a metadata pane modal opens from the breadcrumb. Branch naming uses edit-<slug>-<6hex> to dodge the §19.2 path-routing candidate while preserving §9.5's structural shape. Covered by tests/test_super_draft_vertical.py (10 tests). The full Slices 1-4 suite is 35/35 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
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 §§1–15 of 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.
- 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.
- 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. - 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.
- Super-draft body editing per §9.5 + §9.6. Meta-repo edit branches as the unit of work; everything from §8 inherits.
- Graduation per §13. The dialog, the five-step transactional sequence, rollback, the pre-graduation history affordance.
- Notifications per §15. Last, because every other surface produces signals the inbox receives — notification correctness depends on the producers being in place first.
- The §14 chrome. Landing page polish, the
/philosophyroute, the persistent About link. - 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.
Slice 4 — shipped
Super-draft body editing per §9.5 + §9.6 + §9.7. The §17 routing-collapse
rule landed in backend/app/api_branches.py and backend/app/api_prs.py
— every branches/<branch>/... and prs/<n>/... route now dispatches
on the entry's state to pick the right Gitea repo, and the body
extracted from the entry's frontmatter envelope is what the editor and
the diff see. The bot wrapper grew open_metadata_pr; the rest of the
bot's methods already accepted owner/repo arguments and worked against
the meta repo without change. The §4 cache learned about meta-repo edit
branches via a new refresh_meta_branches pass that mirrors
edit-<slug>-<6hex> branches into cached_branches and synthesizes a
per-slug main row so the §10.1 has-commits-ahead check works
uniformly across active and super-draft surfaces. The §5 schema needed
no migration — the super-draft scoping note already settled that the
existing tables carry both cases.
The two §17 routes Slice 4 added:
| Method | Path | § |
|---|---|---|
| POST | /api/rfcs/{slug}/start-edit-branch |
§9.5 |
| POST | /api/rfcs/{slug}/metadata |
§9.5 |
Everything else from the §8 vertical (chat, accept, decline, manual flush, threads, flags, visibility, grants, the SSE chat stream) and the §10 PR flow (open, draft, review, merge, withdraw, conflict-replay) reaches super-drafts through the same routes Slices 2 and 3 shipped — no per-state forks at the API surface.
The branch-naming choice: §9.5 names the structural shape
edit/<slug>/<auto-name>, but FastAPI's default {branch} path matcher
refuses slashes (the §19.2 path-routing candidate). Slice 4 picked
edit-<slug>-<6hex> — same dash-separated shape Slice 2 used for
<login>-draft-<6hex>. Metadata-pane PRs use the parallel
metadata-<slug>-<6hex> form. The cache parsers in app/cache.py
recognize both the dashed and slashed prefixes so a future routing-fix
slice can flip back without a data migration.
On the frontend, RFCView.jsx's super-draft placeholder was replaced
by the full editor surface — same component, dispatched on
entry.state. The BranchDropdown renders canonical body as the
first position when the entry is a super-draft, per §9.4. A new
MetadataPaneModal opens from the breadcrumb actions when the viewer
holds super-draft edit authority per §9.5 (until §13.1's claim runs,
that's app admins/owners only).
Slice 4 ships covered by backend/tests/test_super_draft_vertical.py —
ten integration tests against the FakeGitea, covering main-view read,
start-edit-branch, body extraction from the envelope on read, accept
preserving the frontmatter on write, manual flush through the envelope,
the body-edit PR's pr_kind='meta_body_edit' shape, the full
cut-accept-open-merge loop with the §9.5 unclaimed-merge gate
(admin/owner only), the metadata pane PR cycle, the canonical-body
branch (main for super-drafts) being read-only, and the metadata pane
permission gate.
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
changeshistory. 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 fromchangesfor 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.pyso the webhook re-registers against the rightAPP_URL.
Conventions
- Bot writes only via
app/bot.py. If a module wants to callapp/gitea.py's write methods directly, the spec is right and the module is wrong — the wrapper is the chokepoint that makes the §6.5On-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 5: graduation per §13.
A super-draft becomes an active RFC through the §13 graduation sequence — the dialog (§13.2), the five-step transactional sequence with rollback (§13.3), the chat-follows-the-work migration (§13.4), the pre-graduation history affordance for the new RFC view (§9.8), and the precondition gate that refuses to graduate while body-edit PRs are open (§9.8 / §13.3).
Slice 4 left this clean: the §9.5 metadata pane, the body-edit PR
flow, and the active-RFC PR flow all converge on the same dispatch.
Graduation is the act that flips an entry's state from super-draft
to active, creates the per-RFC repo via bot.ensure_rfc_repo_seed
(which Slice 2 added as a forward-looking seam), copies the body
from the frontmatter envelope into the new repo's RFC.md, strips
the body field from the meta-repo entry, mints the integer ID and
fills the repo/graduated_at/graduated_by fields, and migrates
the whole-doc main thread's chat to the new RFC's branch_name=null
thread per §13.4.
What Slice 5 owns specifically:
- The §13.2 Graduate dialog — three fields (integer ID, repo name,
initial owners), the inline-validation endpoint
GET /api/rfcs/{slug}/graduate/check, the blocking-PRs popover viaGET /api/rfcs/{slug}/blocking-prs, and the merge-actor set per §13's authority rules. - The §13.3 transactional sequence — five steps emitted as an SSE
stream via
GET /api/rfcs/{slug}/graduate/progress, with each step'spending → running → done/failedtransitions surfacing in the dialog, and a trailingrollbackstep if any earlier step fails. The bot growsgraduateplus the rollback primitives the sequence needs. - The §13.4 chat migration — the whole-doc main thread on the
super-draft (
rfc_slug=<slug>,branch_name='main') re-anchors onto the new RFC's main thread; range and paragraph sub-threads on the canonical-body view migrate too per §9.8's clarification. Edit-branch chats stay attached to their originalbranch_nameon the meta repo per §9.8 — no data movement, surfaced by the pre-graduation history affordance. - The §9.8 pre-graduation history affordance on the new RFC view —
the slug remains the canonical key per §2.3, so the query is a
straightforward lookup of
threadsandchangesrows whererfc_slug = <slug>andbranch_namebegins with the meta-repo edit prefix.
What Slice 5 does NOT own:
- The §15 notification surface (still Slice 6).
- The §14 chrome polish (still Slice 7).
- The §12 30/90 branch-hygiene timers (still Slice 8).
The carryovers Slice 5 inherits — the ensure_rfc_repo_seed
primitive Slice 2 added, the body-edit-PR precondition gate
(checked against the same cached_prs shape Slice 4 wired), and
the existing actions audit-log shape for the rollback record.
The next build session should read SPEC.md, README.md,
docs/DEV.md, and SPEC.md's §19.1 and pick up Slice 5 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.