f67d0aa0db
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
564 lines
30 KiB
Markdown
564 lines
30 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 6 — shipped
|
||
|
||
Notifications per §15 in full, end-to-end against the local Gitea.
|
||
|
||
The producer-side chokepoint lives in
|
||
[`backend/app/notify.py`](../backend/app/notify.py). Every bot
|
||
`_log` call drops into `notify.fan_out_from_action`, which upserts
|
||
the actor's `watches` row per §15.6's substantive-gesture rule and
|
||
runs the §15.1 routing table to insert zero-or-more `notifications`
|
||
rows. Chat-message inserts (the second writer surface, since chat
|
||
doesn't flow through the bot) call `notify.fan_out_chat_message`
|
||
from inside `chat.append_user_message` — same chokepoint shape, one
|
||
place to read the routing. The graduation orchestrator's `_audit`
|
||
helper folds into the same fan-out so `graduate_start` /
|
||
`graduate_complete` ride the chokepoint too.
|
||
|
||
§15.4 email lives in [`backend/app/email.py`](../backend/app/email.py).
|
||
The SMTP adapter wraps Python's `smtplib`; when `SMTP_HOST` is unset
|
||
it falls back to logging the envelope (and appending it to an
|
||
in-memory `_SENT` buffer the integration tests read from). The
|
||
per-category dispatch consults the recipient's toggles, holds
|
||
during §15.8 quiet hours, and on quiet-hours window-end the
|
||
`flush_pending` pass bundles into a single "Activity while you were
|
||
away" mail when more than `EMAIL_BUNDLE_THRESHOLD` accumulated.
|
||
One-click unsubscribe is a signed token over `(user_id, category)`;
|
||
the bounce webhook flips `email_opt_out_all` on the user (new
|
||
column added by migration 008).
|
||
|
||
§15.5 digest lives in [`backend/app/digest.py`](../backend/app/digest.py)
|
||
as a `DigestScheduler` mirroring `cache.Reconciler`'s shape. The
|
||
`run_tick` function is the test seam — integration tests drive
|
||
ticks synchronously, production runs the loop on a `DIGEST_TICK_SECONDS`
|
||
cadence (default 3600s). Each tick releases held emails, decays
|
||
§15.6 `watching` rows whose `last_participation_at` is >90 days
|
||
old, and assembles digests for users whose cadence window has
|
||
rolled over per `notification_digests.period_end`.
|
||
|
||
§15.2 / §15.3 / §15.7 / §15.8 surface as the twelve endpoints in
|
||
[`backend/app/api_notifications.py`](../backend/app/api_notifications.py)
|
||
plus the §15.7 chat-seen advance on `api_branches` and the PR
|
||
seen-cursor advance on `api_prs` — both extended to call
|
||
`notify.reconcile_seen_advance` so visit-advances-cursor closes the
|
||
inbox-read loop per §15.7. The `/api/notifications/stream` SSE
|
||
handler holds a per-user subscriber queue keyed by user_id; one
|
||
event per browser tab, all subscribers for a user receive every
|
||
event so the badge counter stays in lockstep across tabs.
|
||
|
||
| Method | Path | § |
|
||
| ------ | ------------------------------------------------- | ------- |
|
||
| GET | `/api/notifications` | §15.2 |
|
||
| POST | `/api/notifications/{id}/read` | §15.2 |
|
||
| POST | `/api/notifications/read` | §15.2 |
|
||
| GET | `/api/notifications/stream` | §15.3 |
|
||
| GET | `/api/watches` | §15.6 |
|
||
| POST | `/api/rfcs/{slug}/watch` | §15.6 |
|
||
| POST | `/api/rfcs/{slug}/branches/{branch}/chat-seen` | §15.7 |
|
||
| GET | `/api/users/me/notification-preferences` | §15.4/5 |
|
||
| POST | `/api/users/me/notification-preferences` | §15.4/5 |
|
||
| GET | `/api/users/me/quiet-hours` | §15.8 |
|
||
| POST | `/api/users/me/quiet-hours` | §15.8 |
|
||
| POST | `/api/users/{id}/notification-mute` | §15.8 |
|
||
| DELETE | `/api/users/{id}/notification-mute` | §15.8 |
|
||
| GET | `/api/email/unsubscribe` | §15.4 |
|
||
| POST | `/api/webhooks/email-bounce` | §15.4 |
|
||
|
||
On the frontend, `App.jsx` grew a header badge button (`📮` glyph
|
||
with a 99+-capped unread count) that opens the inbox overlay. The
|
||
overlay is `Inbox.jsx` — three filter chips (Unread only, RFC,
|
||
Category) plus a Bundle toggle and a "Mark all read (under filter)"
|
||
action. The badge subscribes to the SSE stream alongside the
|
||
overlay so they share a counter. `ToastHost.jsx` renders personal-
|
||
direct toasts and live-view toasts (an event firing on the slug
|
||
the URL points at), capped at four visible at a time with auto-
|
||
dismiss after a short interval.
|
||
|
||
Slice 6 ships covered by
|
||
[`backend/tests/test_notifications_vertical.py`](../backend/tests/test_notifications_vertical.py) —
|
||
seventeen integration tests covering the producer-side fan-out for
|
||
the propose/merge/decline chain, §15.6 auto-watch on first
|
||
interaction, the §15.2 inbox listing with filter chips, the §15.7
|
||
chat-seen reconciler, the §15.8 per-user mute and the per-RFC mute,
|
||
the §15.4 email-bounce webhook, the `/email/unsubscribe` signed-URL
|
||
path, the §15.8 quiet-hours hold, the §15.5 digest's emit-then-skip
|
||
behavior across two consecutive ticks, preferences and quiet-hours
|
||
round-trips, the explicit-watch override that prevents auto-downgrade,
|
||
and the SSE subscriber/broadcast substrate. The full Slices 1–6 test
|
||
suite is 62/62 green.
|
||
|
||
The schema needed one small migration —
|
||
[`008_email_opt_out.sql`](../backend/migrations/008_email_opt_out.sql)
|
||
adds the `email_opt_out_all` column to `users` for the bounce
|
||
webhook. Topic 13 settled the rest of the §5 surface before the
|
||
build started, so no further migrations were needed.
|
||
|
||
### Slice 5 — shipped
|
||
|
||
Graduation per §13 in full. The §13.3 five-step transactional sequence
|
||
flips a super-draft to active: create the per-RFC repo, seed
|
||
`RFC.md` / `README.md` / `.rfc/metadata.yaml`, open a meta-repo PR
|
||
that strips the entry's body and fills the graduation frontmatter
|
||
(`state: active`, `id: RFC-NNNN`, `repo`, `graduated_at`,
|
||
`graduated_by`), auto-merge that PR with the admin as merge actor,
|
||
refresh the cache so the catalog row and the new RFC view reflect
|
||
`active` immediately. Each step goes through a new bot primitive —
|
||
`create_rfc_repo_for_graduation`, `seed_graduated_rfc`,
|
||
`open_graduation_pr`, `merge_graduation_pr` — that records its own
|
||
row in `actions`, bracketed by `graduate_start` and
|
||
`graduate_complete` for the linkable sequence the §13.3 audit shape
|
||
calls for. The orchestrator in
|
||
[`backend/app/api_graduation.py`](../backend/app/api_graduation.py)
|
||
runs the sequence as an asyncio task fed by an in-memory queue; the
|
||
§17 SSE endpoint subscribes to that queue and emits one event per
|
||
step transition, plus the trailing rollback step's events if any
|
||
earlier step fails.
|
||
|
||
Rollback is per-step and runs in reverse. Each forward step has a
|
||
paired undo registered in `_UNDO_BY_STEP`: `create_repo` → delete the
|
||
repo, `seed_files` → folded into the repo deletion (the seed commits
|
||
live inside the same repo), `open_pr` → close the graduation PR.
|
||
There is no `merge_pr` undo by design — once the meta-repo merge has
|
||
landed, graduation is irreversible per §13.5; the path forward is
|
||
`withdraw` via §3. The rollback also records `graduate_rollback` in
|
||
`actions` with the failed-at step name, the error, and the list of
|
||
undone steps, so the failure surface in the dialog and the `actions`
|
||
log carry the same record.
|
||
|
||
The §9.8 precondition gate — open body-edit PRs against
|
||
`rfcs/<slug>.md` would attempt to re-introduce a body to a
|
||
frontmatter-only entry after step 3 — is enforced before the bot
|
||
starts the sequence, so the §13.3 rollback complexity does not grow.
|
||
The check runs both client-side as the dialog probes
|
||
`GET /api/rfcs/<slug>/blocking-prs` and server-side at the top of
|
||
`POST .../graduate` as an atomic re-check.
|
||
|
||
§13.4 chat migration is a database semantic no-op. The whole-doc
|
||
main thread on the super-draft (`rfc_slug=<slug>`, `branch_name='main'`)
|
||
is the same row interpreted as the super-draft's canonical-body
|
||
thread before graduation and as the new RFC's main thread after —
|
||
the slug is the canonical key per §2.3, the branch_name 'main' now
|
||
points at the per-RFC repo's main, no data movement is needed. Range
|
||
and paragraph sub-threads on the canonical-body view migrate the
|
||
same way per §9.8. Edit-branch chats stay attached to their original
|
||
`branch_name` on the meta repo per §9.8's "no data movement" framing;
|
||
the §9.8 pre-graduation history affordance on the new RFC view
|
||
surfaces them as a distinct disclosure in the breadcrumb dropdown.
|
||
|
||
The §13.1 claim flow landed alongside graduation since claiming is
|
||
the prerequisite for non-admin graduation. The bot grew
|
||
`open_claim_pr`; the existing `api_prs` merge endpoint broadened to
|
||
accept `pr_kind='meta_claim'` so the merge surface inherits
|
||
structurally from §10. Until §13.1's claim runs, the dialog refuses
|
||
the start when `owners=[]` and the popover surfaces "Claim ownership
|
||
yourself" as a remediation affordance — admins are contributors per
|
||
§6.1 and can claim solo if they intend to graduate without further
|
||
ceremony.
|
||
|
||
The five §17 routes Slice 5 added:
|
||
|
||
| Method | Path | § |
|
||
| ------ | ----------------------------------------------- | ------- |
|
||
| POST | `/api/rfcs/{slug}/claim` | §13.1 |
|
||
| GET | `/api/rfcs/{slug}/blocking-prs` | §13.2 |
|
||
| GET | `/api/rfcs/{slug}/graduate/check` | §13.2 |
|
||
| POST | `/api/rfcs/{slug}/graduate` | §13.3 |
|
||
| GET | `/api/rfcs/{slug}/graduate/progress` | §13.3 |
|
||
|
||
On the frontend, `RFCView.jsx`'s breadcrumb actions grew a
|
||
`Graduate to RFC repo` button (admins/owners and entry owners) and
|
||
a `Claim ownership` button (signed-in non-owners). `GraduateDialog.jsx`
|
||
owns the three-field surface with debounced `/check` polling, the
|
||
precondition popover backed by `/blocking-prs`, and the live step
|
||
stack fed by an `EventSource` on the progress SSE. The `BranchDropdown`
|
||
gains a `Pre-graduation history (N)` disclosure that surfaces
|
||
edit-branch threads on the new RFC view per §9.8.
|
||
|
||
Slice 5 ships covered by
|
||
[`backend/tests/test_graduation_vertical.py`](../backend/tests/test_graduation_vertical.py) —
|
||
ten integration tests against the FakeGitea (extended with
|
||
`DELETE /repos/{owner}/{repo}` for the rollback inverse). The tests
|
||
cover the dialog validator's per-field checks, the no-owners
|
||
refusal, the §9.8 open-body-edit-PR precondition refusing the
|
||
start, the §13.3 happy path end-to-end (with audit-log verification),
|
||
mid-sequence rollback at step 2 (seed) and step 3 (PR open), the
|
||
concurrent-graduation refusal, §13.4's chat-row-survives-without-
|
||
data-movement contract, the §9.8 pre-graduation history surface,
|
||
and the §13.1 claim PR cycle. The full Slices 1–5 test suite is
|
||
45/45 green.
|
||
|
||
The orchestrator's `?_sync=1` test seam on `POST .../graduate`
|
||
awaits the sequence inline so integration tests can assert
|
||
post-conditions without driving the SSE. Production clients use the
|
||
spec-described shape — POST returns immediately and the client
|
||
subscribes to the progress SSE.
|
||
|
||
### 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 7: the §14 chrome.**
|
||
|
||
With Slice 6 shipped, every structural and notification beat the
|
||
framework commits to is live: propose, claim, super-draft body
|
||
editing, the §10 PR flow against both repo shapes, graduation, and
|
||
the §15 inbox/email/digest stack. What remains for v1 is the chrome
|
||
that wraps the whole thing — the landing page that brings an
|
||
unauthenticated visitor in, the `/philosophy` route that surfaces
|
||
[`PHILOSOPHY.md`](../PHILOSOPHY.md) verbatim, the persistent About
|
||
link in the header per §14.3, plus the natural neighbors that
|
||
Slice 6 left as API-only and that §19.2 names as candidates:
|
||
|
||
- **The notification-settings surface** — the actual UI for the
|
||
preferences/quiet-hours/mute endpoints Slice 6 wired. Topic 13
|
||
settled the schema and the per-category rules; the surface
|
||
where a contributor finds the per-category email toggles, the
|
||
digest cadence dropdown, the quiet-hours editor, the watches
|
||
overview, and the per-user mute list is the natural follow-on.
|
||
Likely lives at `/settings/notifications` (the link Slice 6's
|
||
emails already point at).
|
||
- **The admin neighborhood.** §19.2's "Admin surfaces" candidate.
|
||
Role management, the §6.2 app-wide write-mute, the audit-log
|
||
viewer, the graduation-readiness queue. Topics 12 and 13 both
|
||
expanded the admin's repertoire without giving it a centralized
|
||
home; Slice 7 picks the framing.
|
||
- **Landing page polish.** Slice 1 stood up a minimal landing for
|
||
the unauthenticated path; §14 commits a richer shape — what the
|
||
framework is, why it exists, what the visitor's first read should
|
||
be, and the sign-in affordance.
|
||
- **The `/philosophy` route.** [`PHILOSOPHY.md`](../PHILOSOPHY.md)
|
||
rendered inline, reachable from the header on every page, so the
|
||
reader can return to the framing without leaving the app.
|
||
|
||
What Slice 7 does NOT own:
|
||
|
||
- The §12 30/90 branch-hygiene timers (still Slice 8).
|
||
- The §16 deferred items.
|
||
- New §15 capabilities — Slice 6 shipped the surface; settings UI
|
||
is exposure of what's already there, not new behavior.
|
||
|
||
The carryovers Slice 7 inherits — the existing §14 spec text, the
|
||
§17 endpoint set including Slice 6's settings endpoints, and the
|
||
React Router layout already in place.
|
||
|
||
The next build session should read `SPEC.md`, `README.md`,
|
||
`docs/DEV.md`, and `SPEC.md`'s §19.1 and pick up Slice 7 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.
|