4565a6cb95
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>
383 lines
20 KiB
Markdown
383 lines
20 KiB
Markdown
# 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`](../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.
|
||
|
||
### 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 `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 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
|
||
via `GET /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's `pending → running → done/failed` transitions surfacing in
|
||
the dialog, and a trailing `rollback` step if any earlier step
|
||
fails. The bot grows `graduate` plus 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 original `branch_name`
|
||
on 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 `threads` and `changes` rows where
|
||
`rfc_slug = <slug>` and `branch_name` begins 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.
|