Slice 5: graduation per §13
The §13.3 transactional sequence flips a super-draft to active — five steps with paired undoes, an in-process orchestrator fed by an asyncio.Queue, the §17 SSE endpoint streaming step transitions to the dialog. Each step is a new bot primitive that logs an `actions` row, bracketed by `graduate_start` / `graduate_complete` for the linkable audit sequence. Rollback runs the undoes in reverse from the last completed step; merge_pr has no undo by design per §13.5. The §9.8 precondition gate is enforced server-side at the top of POST /graduate so the §13.3 rollback complexity does not grow. The §13.4 chat migration is a database semantic no-op — the (slug, branch_name='main') threads keep their identity, only the interpretation changes. The §9.8 pre-graduation history surfaces via a new _is_meta_target(rfc, branch) dispatch helper and lands as pre_graduation_history on /main. §13.1 claim flow landed alongside since it's the prerequisite for non-admin graduation — bot.open_claim_pr plus broadening api_prs._require_pr to accept meta_claim. 45/45 tests green; ten new integration tests cover the validator, the §9.8 precondition refusal, happy path with audit verification, mid-sequence rollback at steps 2 and 3, concurrent refusal, chat-survives-without-data-movement, pre-graduation history, and the §13.1 claim PR cycle. SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew four candidates surfaced during the slice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2405,86 +2405,96 @@ 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: graduation per §13
|
||||
### 19.1 Next slice: notifications per §15
|
||||
|
||||
Slice 4 of the build has landed. Super-draft body editing per §9.5
|
||||
runs end-to-end against the local Gitea — the §9.4 super-draft view
|
||||
replaces the Slice 2 placeholder and renders through the same
|
||||
`RFCView.jsx` surface as an active RFC, dispatched on `entry.state`.
|
||||
The §9.5 `Start Contributing` gesture cuts a meta-repo edit branch
|
||||
via `POST /api/rfcs/<slug>/start-edit-branch`, re-anchors pending
|
||||
main-scoped `changes` rows, and lands the contributor in contribute
|
||||
mode on the new branch. From there everything in §8 — chat, AI
|
||||
participation, accept/decline/edit, manual-edit flushes, range and
|
||||
paragraph sub-threads, flags, DiffView, stale-change handling —
|
||||
reaches the super-draft surface through the same routes Slice 2
|
||||
shipped, with the dispatch sitting in `api_branches.py`'s helpers:
|
||||
when `cached_rfcs.state = 'super-draft'`, the bot writes to the
|
||||
meta repo and the file is `rfcs/<slug>.md` (the body wrapped in
|
||||
frontmatter); when `state = 'active'`, it writes to the per-RFC
|
||||
repo and the file is `RFC.md`. The body extracted from the entry's
|
||||
frontmatter envelope is what the editor and the diff see; the
|
||||
serializer re-wraps on every commit. The §10 PR flow against
|
||||
meta-repo edit branches falls out structurally unchanged, with
|
||||
`pr_kind='meta_body_edit'` distinguishing the cache row — the
|
||||
§10.3 review page, the §10.4 review threads, the §10.5 merge, the
|
||||
§10.8 withdraw, and the §10.9 conflict-replay path all dispatch the
|
||||
same way. §9.7's visibility and contribute grants on edit branches
|
||||
reuse the Slice 2 `branch_visibility` / `branch_contribute_grants`
|
||||
machinery, keyed on the meta repo. The §9.5 metadata pane lands as
|
||||
`POST /api/rfcs/<slug>/metadata` — title and tag edits open a
|
||||
small meta-repo PR via the bot's new `open_metadata_pr` primitive;
|
||||
slug renames remain deferred per §9.5 and the §19.2 candidate. The
|
||||
§9.5 unclaimed-merge gate — only app admins/owners can merge a
|
||||
body-edit PR until §13.1's claim runs — falls out of the existing
|
||||
`_can_merge` rule against an empty `owners_json` / `arbiters_json`.
|
||||
Slice 5 of the build has landed. The §13 graduation flow runs
|
||||
end-to-end against the local Gitea — the Graduate dialog renders
|
||||
the three editable fields (integer ID, repo name, initial owners)
|
||||
with the debounced `GET /api/rfcs/<slug>/graduate/check` lighting
|
||||
up per-field validity inline, the precondition popover surfaces
|
||||
open body-edit PRs via `GET /api/rfcs/<slug>/blocking-prs` (the
|
||||
§9.8 gate enforced before the sequence starts), and confirming the
|
||||
dialog kicks off the §13.3 five-step sequence streamed via
|
||||
`GET /api/rfcs/<slug>/graduate/progress`. The orchestrator in
|
||||
`api_graduation.py` runs the sequence as an asyncio task fed by an
|
||||
in-memory queue; each step's bot primitive
|
||||
(`create_rfc_repo_for_graduation`, `seed_graduated_rfc`,
|
||||
`open_graduation_pr`, `merge_graduation_pr`) lands its own row in
|
||||
`actions`, bracketed by `graduate_start` and `graduate_complete`
|
||||
for the linkable sequence. Rollback is per-step and runs in
|
||||
reverse: each forward step has a paired undo registered in
|
||||
`_UNDO_BY_STEP` — the create-repo undo deletes the repo (which
|
||||
also reclaims the seed commits, so seed-files' undo folds into
|
||||
it), the open-pr undo closes 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 two §17 routes Slice 4 added — `start-edit-branch` and
|
||||
`metadata` — live in `backend/app/api_branches.py`. The bot grew
|
||||
`open_metadata_pr`. The §4 cache grew `refresh_meta_branches`
|
||||
which 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. The §5 schema needed no migration —
|
||||
the super-draft scoping note already settled that the existing
|
||||
tables carry both cases. On the frontend, `RFCView.jsx`'s
|
||||
super-draft placeholder is replaced by the full editor surface;
|
||||
the `BranchDropdown` renders `canonical body` as the first
|
||||
position per §9.4; a `MetadataPaneModal` opens from the breadcrumb
|
||||
actions for viewers holding super-draft edit authority.
|
||||
§13.4's chat migration landed as a database semantic no-op —
|
||||
the whole-doc main thread on the super-draft
|
||||
(`rfc_slug=<slug>`, `branch_name='main'`) is the same row before
|
||||
and after graduation; only the interpretation changes (canonical-
|
||||
body view becomes per-RFC repo's main). The slug is the canonical
|
||||
key per §2.3, so no data movement is needed. Edit-branch chats
|
||||
stay attached to their original `branch_name` 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, with the read path
|
||||
dispatching against the meta repo via a new `_is_meta_target(rfc,
|
||||
branch)` helper that handles both super-draft branches and active-
|
||||
RFC pre-graduation meta-repo branches uniformly.
|
||||
|
||||
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 on read, accept and manual
|
||||
flush both preserving the frontmatter envelope, the body-edit PR's
|
||||
`pr_kind='meta_body_edit'` cache shape, the full cut-accept-open-
|
||||
merge loop with the §9.5 admin-only unclaimed-merge gate, the
|
||||
metadata pane PR cycle, the canonical-body branch (`main` for
|
||||
super-drafts) refusing contribute writes, and the metadata pane
|
||||
permission gate refusing plain contributors. The full Slices 1–4
|
||||
test suite is 35/35 green.
|
||||
The §13.1 claim flow landed alongside graduation since it's the
|
||||
prerequisite for non-admin graduation. The bot grew `open_claim_pr`;
|
||||
`api_prs._require_pr` 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).
|
||||
|
||||
**Slice 5 is graduation per §13.** The five-step transactional
|
||||
sequence flips a super-draft to active: validate the dialog's
|
||||
`id`/`repo`/`owners` inputs against the catalog and Gitea, create
|
||||
the per-RFC repo via `bot.ensure_rfc_repo_seed` (which Slice 2
|
||||
added as a forward-looking seam), copy the body from the entry's
|
||||
frontmatter envelope into the new repo's `RFC.md` on main, strip
|
||||
the body from the meta-repo entry and fill the `id` / `repo` /
|
||||
`graduated_at` / `graduated_by` frontmatter fields, and migrate
|
||||
the chat per §13.4 — the whole-doc main thread and the canonical-
|
||||
body view's range/paragraph sub-threads re-anchor onto the new
|
||||
RFC's main thread; edit-branch chats stay attached to their
|
||||
original `branch_name` on the meta repo per §9.8, surfaced by the
|
||||
pre-graduation history affordance on the new RFC view. The §9.8
|
||||
precondition gate — open body-edit PRs block graduation — is
|
||||
enforced before the bot starts the sequence, so the §13.3 rollback
|
||||
complexity does not grow. The Graduate dialog opens a stream
|
||||
handle for the §17 SSE progress endpoint and renders the step
|
||||
stack from `pending → running → done/failed` transitions, with a
|
||||
trailing `rollback` step's events if any earlier step fails.
|
||||
The five §17 routes Slice 5 added — `claim`, `blocking-prs`,
|
||||
`graduate/check`, `graduate`, and `graduate/progress` — live in
|
||||
`backend/app/api_graduation.py`. The §5 schema needed no migration.
|
||||
On the frontend, `RFCView.jsx`'s breadcrumb actions grew
|
||||
`Graduate to RFC repo` and `Claim ownership` buttons;
|
||||
`GraduateDialog.jsx` owns the three-field surface, the precondition
|
||||
popover, and the live step stack fed by an `EventSource` on the
|
||||
progress SSE; the `BranchDropdown` gains a `Pre-graduation history`
|
||||
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`
|
||||
— ten integration tests against the FakeGitea (extended with
|
||||
`DELETE /repos/{owner}/{repo}` for the rollback inverse) covering
|
||||
the dialog validator's per-field checks, the no-owners refusal,
|
||||
the §9.8 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), concurrent-graduation refusal,
|
||||
§13.4's chat-row-survives 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.
|
||||
|
||||
**Slice 6 is notifications per §15.** Every other vertical now
|
||||
produces signals — propose, claim, merge, graduate, body edits,
|
||||
manual flushes, PR open/withdraw/merge, review threads, conflict-
|
||||
replay — and Slice 6 builds the surface that turns those signals
|
||||
into a contributor's inbox. The §5 schema already carries the
|
||||
notifications, watches, branch_chat_seen, notification_user_mutes,
|
||||
and notification_digests tables; Topic 13's session settled the
|
||||
producer-side rules per §15.1, the §15.2 inbox grouping, §15.3
|
||||
badges and toasts, §15.4 email categories, §15.5 digest cadence,
|
||||
§15.6 watch/subscription, §15.7 unread mechanism, §15.8 do-not-
|
||||
disturb, and §15.9 attribution. The producer-side hook is "after
|
||||
a write succeeds, evaluate watches and fan-out notification rows"
|
||||
— same chokepoint shape Slice 1's `_log` uses, invoked inline
|
||||
from the bot wrapper. The consumer-side hook is the header badge,
|
||||
the inbox panel, the toast surface, and the per-row read-state
|
||||
machinery. The §15.4 email loop and the §15.5 digest are the
|
||||
heavier sub-pieces — the digest needs a scheduled-job runner;
|
||||
the email loop needs a transactional-email adapter and the
|
||||
`POST /api/webhooks/email-bounce` receiver.
|
||||
|
||||
The next build session should read `SPEC.md`, `README.md`,
|
||||
`docs/DEV.md`, and this §19.1 entry and pick up Slice 5 cleanly
|
||||
`docs/DEV.md`, and this §19.1 entry and pick up Slice 6 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
|
||||
@@ -2688,6 +2698,54 @@ binding.
|
||||
cheap, but a dedicated index on `cached_branches.branch_name`
|
||||
would shorten the join-against-`cached_rfcs`-state for very
|
||||
large super-draft fleets. Trivial; defer until the cost shows up.
|
||||
- **Graduation progress persistence across page reloads.** Slice 5's
|
||||
orchestrator holds the per-step state in a process-local dict
|
||||
keyed by slug, fed by an asyncio.Queue the §17 SSE drains. A page
|
||||
reload while the sequence is in flight loses the dialog but the
|
||||
sequence continues to completion on the server; the user can
|
||||
reopen the dialog and the snapshot event re-renders the current
|
||||
state (the in-memory entry persists until cleanup). What is not
|
||||
yet settled: how long to retain the entry after `finished=True`,
|
||||
whether to persist enough on `actions` to reconstruct the step
|
||||
stack from the audit log for a returning admin who missed the
|
||||
live stream, and whether the dialog should re-open automatically
|
||||
on a slug whose registry entry is still present. Touches §13.3
|
||||
(the rendered step stack's durability) and §15.2 (a graduation-
|
||||
in-progress signal as an inbox row would be a natural alternative
|
||||
surface for follow-along). Earns its own topic once the build
|
||||
hits cases where a single sequence runs long enough that the
|
||||
reload-during-graduation path matters.
|
||||
- **Graduation PR auto-close on rollback's `close_graduation_pr`.**
|
||||
Slice 5's rollback closes the graduation PR via `gitea.close_pull`
|
||||
but leaves the dash-suffixed branch (`graduate-<slug>-<6hex>`)
|
||||
in place. The branch is short-lived in steady-state — graduation
|
||||
succeeds — but accumulated failed-graduation branches over time
|
||||
could clutter the meta repo's branch list. The §12 30/90 hygiene
|
||||
timers (Slice 8) would naturally sweep them, but a graduation-
|
||||
specific cleanup that deletes the branch on rollback would close
|
||||
the loop faster. Trivial to add when evidence shows the branches
|
||||
pile up.
|
||||
- **The `_is_meta_target(rfc, branch)` dispatch helper.** Slice 5
|
||||
generalized the super-draft routing collapse (`_is_super_draft`
|
||||
alone) to also handle active-RFC reads against pre-graduation
|
||||
meta-repo branches per §9.8. The helper checks state plus a name-
|
||||
prefix list (`edit-`, `edit/`, `metadata-`, `metadata/`, `claim/`,
|
||||
`propose/`, `graduate-`). The prefix list is right for v1's
|
||||
surface, but a contributor renaming an active-RFC per-RFC branch
|
||||
to one of those prefixes would have reads route to the meta repo
|
||||
(and likely 404). The `_validate_branch_name` guard refuses
|
||||
reserved prefixes on creation, so the only way to reach this
|
||||
edge is a hand-renamed branch — defer-able until evidence shows
|
||||
it happens.
|
||||
- **Test seam for synchronous graduation.** Slice 5's `?_sync=1`
|
||||
query param on `POST .../graduate` awaits the orchestrator inline
|
||||
so integration tests can assert post-conditions without driving
|
||||
the SSE. The seam is documented in code; it does not affect the
|
||||
production path. A cleaner long-term shape is to expose the
|
||||
orchestrator as importable for tests and remove the query-param
|
||||
branch from the route handler, but the current shape is the
|
||||
minimum that keeps the test surface terse without adding a
|
||||
separate test-only module.
|
||||
- **Body full-text search.** When the time comes.
|
||||
|
||||
Topic 13 (notifications) is settled and folded into §5 (the
|
||||
|
||||
Reference in New Issue
Block a user