1b0968a9a2
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>
504 lines
27 KiB
Markdown
504 lines
27 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 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 6: 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, super-draft chat. Slice 6 builds
|
||
the inbox, the fan-out, the digest, and the email loop that turn
|
||
those signals into a contributor's surface. 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 signal-surface
|
||
stack), 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.
|
||
|
||
Slices 1–5 left this clean: every user gesture goes through the
|
||
bot wrapper and lands an `actions` row with the underlying actor.
|
||
The producer-side hook is "after a write succeeds, evaluate watches
|
||
and fan-out notification rows." The consumer-side hook is the
|
||
header badge, the inbox panel, the toast surface, and the per-row
|
||
read-state machinery.
|
||
|
||
What Slice 6 owns specifically:
|
||
|
||
- **The producer fan-out.** Every `actions` row whose event maps to a
|
||
§15 signal produces zero-or-more `notifications` rows by joining
|
||
against `watches` and applying the §15.1 priority rules. The
|
||
fan-out lives as a small module that the bot wrapper invokes
|
||
inline after each write — same chokepoint shape Slice 1's
|
||
`_log` uses.
|
||
- **The §15.2 inbox.** `GET /api/notifications` with the
|
||
`unread` / `rfc_slug` / `category` / `bundled` filter chips,
|
||
`POST /api/notifications/<id>/read` for per-row marking,
|
||
`POST /api/notifications/read` for the bulk filter mark, and the
|
||
SSE `GET /api/notifications/stream` that backs the live badge.
|
||
- **The §15.3 surface.** The header badge counter (live via the SSE),
|
||
the toast on personal-direct events while the user is active, and
|
||
the ambient signal — a colored dot per row on the §7 catalog
|
||
pointing at watched RFCs with unseen activity.
|
||
- **The §15.4 email loop.** Per-category opt-in/out preferences on
|
||
the users table (already in the schema), the `/api/users/me/notification-preferences`
|
||
endpoints, the email-send adapter that routes a notification's
|
||
category through the user's category toggle, and the
|
||
`POST /api/webhooks/email-bounce` receiver that sets the global
|
||
opt-out. Plus the `GET /api/email/unsubscribe` signed-URL
|
||
one-click flow.
|
||
- **The §15.5 digest.** A scheduled-job that runs daily and weekly
|
||
to roll up unseen notifications into a single email, with the
|
||
`notification_digests` table tracking what was included so the
|
||
next digest skips what already shipped.
|
||
- **The §15.6 watch model.** Auto-watch on first interaction with
|
||
an RFC, the per-row state column (`watching` / `following` /
|
||
`muted`), the 90-day auto-decay for unset rows, and the explicit
|
||
`POST /api/rfcs/<slug>/watch` overrides.
|
||
- **The §15.7 unread mechanism.** Advance the `branch_chat_seen`
|
||
cursor on every branch read, reconcile inbox notifications to
|
||
read when their underlying surface is consumed.
|
||
- **The §15.8 do-not-disturb.** Quiet-hours config on the user, the
|
||
per-user notification mute list, the orthogonality vs §6.2's
|
||
app-wide write-mute.
|
||
|
||
What Slice 6 does NOT own:
|
||
|
||
- The §14 chrome polish (still Slice 7).
|
||
- The §12 30/90 branch-hygiene timers (still Slice 8).
|
||
- The §16 deferred items.
|
||
|
||
The carryovers Slice 6 inherits — the existing `actions` audit log
|
||
(every signal traces back to a row there per §15.9), the SSE
|
||
machinery from Slices 2 and 5 (chat-stream and graduate-progress
|
||
respectively), and the §5 schema's notification tables (already
|
||
in place from Topic 13).
|
||
|
||
The §15 surface depends on the producers being in place; with
|
||
Slice 5 landing the last structural producer (graduation events,
|
||
specifically `graduate_complete` as a personal-direct event for
|
||
the proposer per §15.4), every signal a contributor needs to see
|
||
is now in the audit log waiting to be fanned out.
|
||
|
||
The next build session should read `SPEC.md`, `README.md`,
|
||
`docs/DEV.md`, and `SPEC.md`'s §19.1 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
|
||
candidate topics in §19.2, do not extend the spec beyond what the
|
||
slice requires.
|