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>
2724 lines
139 KiB
Markdown
2724 lines
139 KiB
Markdown
# RFC App — Specification
|
||
|
||
This is the agreed-upon model for the rewrite of the Wiggleverse RFC Contributor app.
|
||
It captures the structural decisions made before any UX work on the main document
|
||
pane, per-RFC conversations, revisions, and PRs. Those areas are deliberately
|
||
out of scope here and will be designed in a follow-up session that takes this
|
||
spec plus the existing prototype as context.
|
||
|
||
The technical stack is unchanged from the prototype: FastAPI with SSE streaming
|
||
on the backend, React + Vite + Tiptap (ProseMirror) on the frontend, Gitea as
|
||
the Git host, multiple LLM providers (Anthropic, Google, OpenAI / GitHub
|
||
Copilot). The code base is a clean rewrite; the existing app is reference only.
|
||
|
||
---
|
||
|
||
## 1. Repository topology
|
||
|
||
Each RFC is its own Gitea repository. There is in addition exactly one **meta
|
||
repository** that serves as the authoritative directory of all RFCs in the
|
||
system — drafts, active work, and retired entries alike.
|
||
|
||
All Git operations across all repositories are performed by a single **bot
|
||
service account** in Gitea. Real human users do not have meaningful Gitea
|
||
permissions on the repos themselves; their accounts exist for OAuth identity
|
||
only. The bot is the author of every commit, the opener of every PR, and the
|
||
merger of every merge. Authorization decisions are made by the app, in app
|
||
data, *before* the bot acts on the user's behalf.
|
||
|
||
This means the only contribution surface is the app itself. Raw `git clone`
|
||
plus `git push` is not a supported contribution path. This is a deliberate
|
||
tradeoff: in exchange for losing that path, we gain a permission model that
|
||
can express things Gitea cannot (per-branch contribute grants, per-RFC
|
||
delegated ownership, per-user capability overrides) without contorting Gitea.
|
||
|
||
---
|
||
|
||
## 2. The meta repository's schema
|
||
|
||
The meta repo's `main` branch contains:
|
||
|
||
- `rfcs/` — exactly one markdown file per RFC entry, regardless of state.
|
||
Filenames are the entry's slug, e.g. `rfcs/open-human-model.md`. Slugs are
|
||
the stable identifier from the moment an idea is proposed; integer
|
||
`RFC-NNNN` IDs are only assigned at graduation (see §13).
|
||
- `PHILOSOPHY.md` — the framework's mission and rationale. Hand-authored.
|
||
Updated via PR like any other document. The canonical statement of why
|
||
this exists and what it is producing. Its opening section is a short-form
|
||
distillation (the deck) suitable for use both as the meta-repo README
|
||
header and as the app's pre-login landing copy (see §14).
|
||
- `README.md` — the meta-repo's front page. The **header is hand-authored**
|
||
— a short-form distillation of `PHILOSOPHY.md`, suitable for a reader
|
||
arriving at the meta-repo via Git rather than via the app. The **index
|
||
below the header is regenerated by CI on every merge to main** and lists
|
||
active RFCs, super-drafts, and (eventually) retired entries with links
|
||
into the corresponding entry files and, when present, the RFC's own
|
||
repository.
|
||
- `CONTRIBUTING.md` — explains how to propose, claim, and contribute.
|
||
- A workflow file (Gitea Actions) that regenerates the README index.
|
||
|
||
That's the entirety of the meta repo. App-level permission state, user
|
||
accounts, chat history, audit logs, and branch visibility grants do **not**
|
||
live in the meta repo — they live in the app database (see §5).
|
||
|
||
### 2.1 Entry file format
|
||
|
||
```markdown
|
||
---
|
||
slug: open-human-model
|
||
title: Open Human Model
|
||
state: super-draft # super-draft | active | withdrawn
|
||
id: null # null until graduated; then "RFC-0042"
|
||
repo: null # null until graduated; then "wiggleverse/rfc-0042-open-human-model"
|
||
proposed_by: ben@wiggleverse.org
|
||
proposed_at: 2026-05-22
|
||
graduated_at: null
|
||
graduated_by: null
|
||
owners: [] # contributors elevated for this RFC
|
||
arbiters: [ben] # contributors with merge authority for this RFC
|
||
tags: [identity, schema]
|
||
---
|
||
|
||
## Why this RFC is needed
|
||
|
||
(One- or two-paragraph pitch from the proposer. While the entry is a
|
||
super-draft, the body may grow into the actual draft document. On
|
||
graduation, this body migrates to RFC.md in the new repo; see §13.)
|
||
```
|
||
|
||
### 2.2 Idea submission as PR
|
||
|
||
Proposing a new RFC means opening a PR against the meta repo that adds one
|
||
new file under `rfcs/`. This is what the "Propose new RFC" button in the
|
||
left pane drives. One file per PR keeps idea submissions atomic and
|
||
conflict-free even with concurrent proposers.
|
||
|
||
### 2.3 No integer ID until graduation
|
||
|
||
Integer `RFC-NNNN` IDs are assigned at graduation time as `max(existing
|
||
integer IDs) + 1`. Super-drafts have no integer ID; their stable identifier
|
||
is the slug. This avoids the race condition the prototype has (where two
|
||
concurrent submissions can try to claim the same number) and means
|
||
proposing an idea costs nothing in identifier space.
|
||
|
||
---
|
||
|
||
## 3. RFC states
|
||
|
||
There are three canonical states stored in entry frontmatter:
|
||
|
||
- **`super-draft`** — the entry exists in the meta repo's `rfcs/`
|
||
directory. No dedicated repo yet. Anyone signed in can chat on it; anyone
|
||
can claim ownership; an owner is required before graduation.
|
||
- **`active`** — the entry has been graduated. A dedicated RFC repo
|
||
exists, `repo:` points to it, and real branches/PRs/conversation happen
|
||
there.
|
||
- **`withdrawn`** — pulled before becoming canonical. Stays in the
|
||
directory as historical record, hidden from default views, filterable in.
|
||
|
||
A fourth concept — **`idea`** — is not stored in frontmatter. It is the
|
||
*derived view* of "there is an open PR against the meta repo proposing
|
||
this entry." Pending ideas surface in the left pane through a separate
|
||
affordance (§7), not by reading frontmatter.
|
||
|
||
Two further states (`accepted`, `deprecated`) are deliberately deferred.
|
||
The OHM process does not currently need a hard "this is now official"
|
||
moment; `active` covers that role implicitly. We will revisit if a real
|
||
need emerges.
|
||
|
||
### 3.1 Legal transitions and who can perform them
|
||
|
||
```
|
||
(no entry) ──[idea PR merged by owner/admin]──▶ super-draft
|
||
super-draft ──[graduate, owner or admin]─────▶ active
|
||
super-draft ──[withdraw, proposer or owner/admin]──▶ withdrawn
|
||
active ──[withdraw, owner/admin]─────────────▶ withdrawn
|
||
withdrawn ──[reopen, owner/admin]────────────▶ super-draft
|
||
```
|
||
|
||
Every transition is a commit to the meta repo. State history is auditable
|
||
through `git log rfcs/<slug>.md`. The app maintains a separate audit log
|
||
for "who clicked the button" (see §6.5).
|
||
|
||
### 3.2 State change side-effects
|
||
|
||
For now, changing state in the meta repo entry is the *only* required
|
||
operation for a state transition. Graduation has additional side effects
|
||
(creating the new repo, seeding it); those are covered in §13. We
|
||
deliberately do not tag commits, lock branches, or post notices on
|
||
state change for now — the entry frontmatter is the single source of
|
||
truth and any further automation is a later refinement.
|
||
|
||
---
|
||
|
||
## 4. Storage architecture: Git is truth, app keeps a cache
|
||
|
||
Gitea remains the source of truth for everything Git-shaped: meta repo
|
||
content, RFC repo content, branches, PRs, commits. Nothing in this system
|
||
overrides Gitea on those concerns.
|
||
|
||
The app maintains a **SQLite database**, colocated with the FastAPI
|
||
process, that serves three purposes:
|
||
|
||
1. **Metadata cache** — mirrors only what the left pane and tree view
|
||
need to render fast. Reconstructible from Gitea at any time.
|
||
2. **App data** — things Gitea cannot express. User accounts, role
|
||
assignments, per-branch grants, branch visibility settings, chat
|
||
history, audit logs. This data is canonical; it is not cached, it
|
||
is owned by the app.
|
||
3. **Cached bodies** — the main-branch body of each RFC's `RFC.md` (and
|
||
each super-draft's entry body) is cached for left-pane previews and
|
||
read-without-roundtrip. Branch bodies are *not* cached; the editor
|
||
fetches them live from Gitea when opened.
|
||
|
||
### 4.1 Cache freshness
|
||
|
||
Two paths keep the cache current, running in parallel:
|
||
|
||
- **Gitea webhooks** push events on every meaningful change: meta-repo
|
||
merge, branch create/push/delete, PR open/close/merge, repo create.
|
||
A webhook handler does a focused re-read of just what changed.
|
||
Typical latency: sub-second.
|
||
- **A periodic reconciler** runs every five minutes and does a full
|
||
sweep — list meta-repo entries, list each RFC repo's branches and
|
||
PRs, diff against the cache, fix drift. This is the safety net for
|
||
missed webhooks and downtime.
|
||
|
||
The app never writes to the cache from user actions directly. All cache
|
||
writes flow from webhook arrival or reconciler runs, both of which read
|
||
from Gitea. If the cache is lost or corrupted, the reconciler rebuilds
|
||
from scratch.
|
||
|
||
### 4.2 Operational shape
|
||
|
||
SQLite for now. Colocated with the FastAPI process. Single file, backed
|
||
up alongside the rest of the app. We accept the implication that the
|
||
app can only run as a single process for the foreseeable future. If we
|
||
outgrow this, we plan a maintenance window and migrate to Postgres on
|
||
a separate host.
|
||
|
||
Body full-text search is not part of v1. Title, ID, slug, and tag search
|
||
hit the cache directly. If/when we want full-text search over RFC bodies,
|
||
the natural path is SQLite FTS5 indexed off the reconciler.
|
||
|
||
---
|
||
|
||
## 5. The app's data model (canonical app tables)
|
||
|
||
These are the tables that are app-owned (not cached from Gitea). Names
|
||
and exact columns are illustrative; the implementing session can adjust.
|
||
|
||
- `users` — `id`, `email`, `display_name`, `gitea_login`, `role` (one
|
||
of `owner` / `admin` / `contributor`), `muted` (bool — the §6.2
|
||
app-wide write-mute, distinct from the per-RFC and per-user
|
||
notification mutes in §15.8), `created_at`, `last_seen_at`. Plus the
|
||
per-user notification preferences inlined here for proximity:
|
||
`email_personal_direct` (bool, default true), `email_watched_structural`
|
||
(bool, default false), `email_admin_actionable` (bool, default true for
|
||
admins/owners and unused for contributors), `digest_cadence`
|
||
(`off` / `weekly` / `daily`, default `weekly`),
|
||
`notification_quiet_hours_start` (nullable time),
|
||
`notification_quiet_hours_end` (nullable time),
|
||
`notification_quiet_hours_timezone` (nullable text, IANA tz name).
|
||
The watched-RFC-churn category has no column — it is permanently off
|
||
per §15.4 and surfaces in settings as a disabled toggle naming the
|
||
refusal.
|
||
- `branch_visibility` — `id`, `rfc_slug`, `branch_name`, `read_public`
|
||
(bool, default true), `contribute_mode` (`just-me` / `specific` /
|
||
`any-contributor`, default `just-me`).
|
||
- `branch_contribute_grants` — `id`, `rfc_slug`, `branch_name`,
|
||
`grantee_user_id`, `granted_by`, `granted_at`.
|
||
- `stars` — `id`, `user_id`, `rfc_slug`, `starred_at`.
|
||
- `threads` — every conversation in the system, whether scoped to an RFC's
|
||
main view, a branch, or a span within a branch's document. Columns:
|
||
`id`, `rfc_slug`, `branch_name` (nullable — null means scoped to the
|
||
RFC's main view), `anchor_kind` (`whole-doc` | `range` | `paragraph`),
|
||
`anchor_payload` (JSON: serialized ProseMirror range or paragraph id),
|
||
`thread_kind` (`chat` | `flag` | `review` — `review` is the diff-anchored
|
||
PR-review thread defined in §10.4), `label` (short human-authored summary;
|
||
for flags this is the entire content), `state` (`open` | `resolved` |
|
||
`stale`), `created_by`, `created_at`, `resolved_at`, `resolved_by`.
|
||
Visibility is derived from the underlying branch (§11.1).
|
||
- `thread_messages` — the actual chat content for `chat`-kind threads.
|
||
Columns: `id`, `thread_id`, `role` (`user` | `assistant` | `system`),
|
||
`author_user_id` (nullable; null for assistant), `model_id` (nullable;
|
||
set when `role = assistant`), `text`, `quote` (the optional selection
|
||
the user attached to the message), `created_at`. Flag-kind threads
|
||
have no rows here unless they get converted to chats, at which point
|
||
messages accumulate normally. System-author messages (`role = system`,
|
||
`author_user_id = null`) are also used to mark manual-edit flushes on
|
||
PRs per §10.6.
|
||
- `changes` — structured proposed edits to a branch's document. Each
|
||
change is either AI-proposed (from a `<change>` block in an assistant
|
||
message, per the protocol carried over from the prototype, §18) or
|
||
manually authored (typed directly into the editor). Columns: `id`,
|
||
`rfc_slug`, `branch_name`, `thread_id` (nullable; null for direct
|
||
manual edits not tied to a thread), `source_message_id` (nullable;
|
||
set for AI-proposed changes), `kind` (`ai` | `manual`), `state`
|
||
(`pending` | `accepted` | `declined`), `original`, `proposed`,
|
||
`reason`, `was_edited_before_accept` (bool), `stale_since` (nullable
|
||
timestamp; set when an AI proposal's `original` no longer matches the
|
||
branch's current text because a subsequent manual edit overlapped it,
|
||
per §8.11; orthogonal to `state`, which stays `pending` until the
|
||
contributor acts on the stale card), `acted_by`, `acted_at`,
|
||
`commit_sha` (nullable; the bot commit that materialized the
|
||
acceptance, see §8.6). The `<change>` / `<original>` / `<proposed>`
|
||
/ `<reason>` parsing protocol is unchanged from the prototype.
|
||
- `pr_seen` — per-user, per-PR seen-cursor for the "what changed since
|
||
my last visit" mechanism in §10.3. Columns: `id`, `user_id`, `rfc_slug`,
|
||
`pr_number`, `last_seen_commit_sha`, `last_seen_message_id`, `seen_at`.
|
||
Advanced on view; one row per (user, PR).
|
||
- `branch_chat_seen` — per-user, per-branch-chat seen-cursor, the
|
||
within-branch-chat sibling of `pr_seen`. Columns: `id`, `user_id`,
|
||
`rfc_slug`, `branch_name`, `last_seen_message_id`, `seen_at`. Advanced
|
||
on view; one row per (user, branch). Supports the in-context "new
|
||
messages since last visit" accent on branch chats and closes the
|
||
inbox-reconciliation loop for chat-kind notifications per §15.7.
|
||
- `watches` — per-user, per-RFC subscription state for the implicit
|
||
watch model in §15.6. Columns: `id`, `user_id`, `rfc_slug`, `state`
|
||
(`watching` | `following` | `muted`), `set_by` (`auto` | `explicit`),
|
||
`set_at`, `last_participation_at` (nullable; used by the 90-day
|
||
decay rule in §15.6). One row per (user, RFC); absent row means
|
||
no relationship (no signals generated).
|
||
- `notifications` — per-recipient, per-event signal rows that back the
|
||
inbox, badges, email, and digest per §15. Columns: `id`,
|
||
`recipient_user_id`, `event_kind` (enum, see §15.1), `rfc_slug`
|
||
(nullable), `branch_name` (nullable), `pr_number` (nullable),
|
||
`thread_id` (nullable), `change_id` (nullable), `actor_user_id`
|
||
(nullable — null for system-generated events per §15.9), `payload`
|
||
(JSON: the short rendered text the inbox row and email body share,
|
||
plus any kind-specific extras), `created_at`, `read_at` (nullable),
|
||
`email_sent_at` (nullable), `digest_included_at` (nullable). Fan-out
|
||
is at signal-generation time per §15.7.
|
||
- `notification_digests` — per-recipient digest emissions, used by §15.5's
|
||
event-window dedup. Columns: `id`, `recipient_user_id`, `sent_at`,
|
||
`period_start`, `period_end`, `signal_ids_included` (JSON array of
|
||
notification ids). One row per emitted digest.
|
||
- `notification_user_mutes` — per-recipient mute of notifications
|
||
produced by a specific actor, per §15.8. Columns: `id`,
|
||
`muter_user_id`, `muted_user_id`, `muted_at`. Notification-volume
|
||
only; does not affect content visibility anywhere in the app. Not
|
||
available to arbiters or admins acting in their authority capacity
|
||
on RFCs where they hold authority (§6.3) — the role contractually
|
||
requires receiving signals from everyone.
|
||
- `permission_events` — append-only audit log for every role change and
|
||
capability override.
|
||
- `actions` — append-only audit log for every state transition, every
|
||
graduation, every grant change. Includes the acting user, the bot
|
||
commit hash if any, and the on-behalf-of trailer applied.
|
||
|
||
**Super-draft scoping.** For rows in `threads` and `changes` where the
|
||
entry referenced by `rfc_slug` is in state `super-draft`, `branch_name`
|
||
names a branch on the **meta repo** rather than on a per-RFC repo —
|
||
see §9.5 for the unit-of-work mapping. No column changes are required;
|
||
the interpretation flows from the entry's state. Threads on a
|
||
pending-idea PR (see §9.3) carry the proposed slug as `rfc_slug`
|
||
pre-merge — slugs are reserved during the idea PR per §9.1's
|
||
uniqueness check — and become the super-draft's main-chat threads on
|
||
merge with no data movement.
|
||
|
||
---
|
||
|
||
## 6. Permission model
|
||
|
||
Authorization is owned by the app. Gitea sees only the bot account.
|
||
|
||
### 6.1 Four roles, each a strict superset of the one below
|
||
|
||
1. **Anonymous.** Can read public RFCs (the meta repo's main branch,
|
||
every RFC repo's main branch), read any branch whose `read_public`
|
||
is true, read any PR. Cannot chat, propose, create branches, or
|
||
open PRs.
|
||
2. **Contributor.** Default role for any authenticated account.
|
||
Everything anonymous can do, plus: propose new RFCs (open a PR
|
||
against the meta repo), create branches on any RFC repo, open PRs
|
||
from branches they have contribute access to, chat on anything
|
||
they can read, claim ownership of unclaimed super-drafts.
|
||
3. **Admin.** Everything contributor can do, plus: act on any RFC
|
||
(merge PRs on behalf of arbiters, graduate super-drafts, set
|
||
branch visibility on anyone's behalf, downgrade or restore
|
||
individual contributor capabilities, grant or revoke admin to
|
||
others, withdraw or reopen entries).
|
||
4. **Owner.** Everything admin can do, plus: grant or revoke owner,
|
||
disable an account entirely. Ben is owner zero.
|
||
|
||
### 6.2 Per-user capability overrides — "muted"
|
||
|
||
Owners and admins can mute a contributor. A muted user retains read
|
||
access and existing branches, but cannot create new branches, open
|
||
PRs, propose RFCs, or chat. Existing branches remain in the system
|
||
subject to the standard 30/90 hygiene rules (§12). Restoring is the
|
||
reverse action. Every mute and restore is logged in
|
||
`permission_events`.
|
||
|
||
This write-mute is structurally distinct from the two notification
|
||
mutes introduced in §15.8 — the per-RFC notification mute (the
|
||
`muted` state on the `watches` row, §15.6) and the per-user
|
||
notification mute (`notification_user_mutes`, §15.8). The three
|
||
share a word and nothing else: the write-mute is an admin-imposed
|
||
restriction on a contributor's ability to act; the notification
|
||
mutes are self-imposed preferences about receiving signals. They
|
||
live in separate columns and never gate each other. A write-muted
|
||
contributor continues to receive notifications normally (so they can
|
||
triage what they can't act on, and the restore lands cleanly); a
|
||
self-DND'd contributor's own gestures continue to fire signals to
|
||
others normally.
|
||
|
||
### 6.3 Per-RFC delegated authority
|
||
|
||
An RFC's `owners:` and `arbiters:` (from the meta-repo entry's
|
||
frontmatter) are contributors who, **within that RFC**, can perform
|
||
admin-scope actions: grant contribute access on any branch in the
|
||
RFC, merge PRs, set branch visibility, withdraw the RFC. They are
|
||
not app-wide admins; their elevated powers are scoped to that
|
||
single RFC. This is what lets work distribute without Ben being on
|
||
the hook for every action.
|
||
|
||
### 6.4 Per-branch contribute grants
|
||
|
||
Each branch has a `contribute_mode`:
|
||
|
||
- `just-me` (default) — only the branch creator can push.
|
||
- `specific` — only the branch creator and the users in
|
||
`branch_contribute_grants` can push.
|
||
- `any-contributor` — any signed-in contributor can push (the
|
||
"I want help" mode).
|
||
|
||
The branch creator and the RFC's owners/arbiters can change this
|
||
setting at any time. Anonymous users can never push regardless of
|
||
setting.
|
||
|
||
### 6.5 Audit trail
|
||
|
||
Every commit the bot makes carries an `On-behalf-of:` trailer naming
|
||
the acting user. Gitea's commit log is for code archaeology; the
|
||
`actions` and `permission_events` tables are the real accountability
|
||
record.
|
||
|
||
---
|
||
|
||
## 7. The left pane
|
||
|
||
The default view is a single scrollable flat list of every meta-repo
|
||
entry whose state is `super-draft` or `active`. State is conveyed by
|
||
visual cue on each row (muted styling and a "super-draft" tag for
|
||
super-drafts; normal weight for active; an integer ID badge for active
|
||
entries only). The list is **not** grouped by state — grouping forces
|
||
a hierarchy on the user that gets in the way of finding by title.
|
||
|
||
### 7.1 Above the list
|
||
|
||
- **Search box** — fuzzy-matches title, slug, and integer ID.
|
||
- **Sort dropdown** — default "Recently active" (max of the RFC's
|
||
last main-branch merge and the meta-repo entry's last commit, for
|
||
super-drafts with no repo). Other options: Created date, Title,
|
||
ID, State.
|
||
- **Filter chip strip** — multi-select, AND-combined. Chips:
|
||
`State: super-draft | active | withdrawn`, `My RFCs` (I'm an owner
|
||
or arbiter), `Has open PRs`, `Unclaimed` (super-drafts with empty
|
||
`owners:`), `Tag: …`.
|
||
|
||
### 7.2 The list rows
|
||
|
||
Active entries render as `RFC-0042 · Open Human Model`. Super-drafts
|
||
render as `super-draft · Open Human Model` (no integer ID, by design).
|
||
A small star icon on the row left-edge for any RFC the signed-in
|
||
user has starred — starred RFCs pin to the top of the current sort
|
||
order. A small unseen-activity dot — binary, not a count — appears on
|
||
the row's right edge for any RFC the signed-in user is `watching` or
|
||
`following` (per §15.6) that has at least one unread notification
|
||
since the user's last visit. The dot is the only per-row notification
|
||
signal the catalog carries; counts per row would turn the catalog
|
||
into a leaderboard, which is the engagement-bait failure mode §15.1
|
||
refuses.
|
||
|
||
When a super-draft graduates per §13, the row's transition from
|
||
`super-draft · Title` to `RFC-NNNN · Title` renders as a brief
|
||
crossfade — on the order of 300–400ms — with the integer-ID badge
|
||
animating into the row's left edge as the muted super-draft styling
|
||
fades to the standard weight. The transition fires on the first
|
||
render after the post-graduation webhook for every viewer whose
|
||
catalog is currently mounted, not just the admin who confirmed; a
|
||
contributor browsing the catalog when graduation lands sees the
|
||
same acknowledgment. The animation is the catalog's one signal that
|
||
a structural act happened; the in-dialog "Graduation complete" frame
|
||
from §13.3 carries the ceremonial beat for the confirming admin.
|
||
|
||
### 7.3 Below the list
|
||
|
||
- **"Pending ideas" disclosure** — a small expandable section showing
|
||
open meta-repo PRs that are proposing new entries. This is the only
|
||
place idea-state items appear. Reading is open to all; reacting in
|
||
the PR discussion requires contributor; merging requires
|
||
owner/admin.
|
||
- **"+ Propose new RFC" button** — kicks off idea submission, which
|
||
is a PR against the meta repo adding one file to `rfcs/`.
|
||
|
||
### 7.4 Drilling into an RFC
|
||
|
||
Selecting an entry opens its RFC view. The structural shape of that
|
||
view — a tree of main / open PRs / open branches / closed-but-not-
|
||
deleted branches (hidden by default) — is established here. The
|
||
document pane, chat, and branch navigation inside that view are
|
||
specified in §8. The revision flow and PR flow inside the view
|
||
remain deferred (see §16).
|
||
|
||
---
|
||
|
||
## 8. The RFC view: document, chat, and branch navigation
|
||
|
||
When a user selects an active RFC from the left pane, the app opens
|
||
its RFC view. This section captures the structural decisions about
|
||
that view: layout, the document, the chat, and how the user moves
|
||
between branches.
|
||
|
||
### 8.1 Layout
|
||
|
||
The RFC view inherits the three-column shape from the prototype:
|
||
|
||
- **Left column** — the RFC catalog from §7, unchanged.
|
||
- **Center column** — a thin breadcrumb strip at the top showing the
|
||
current branch with a dropdown affordance; the editor (or diff view
|
||
in review mode) below it; a prompt bar at the bottom for chat input.
|
||
- **Right column** — the chat thread for the currently-selected
|
||
branch, with a change-card panel below it in contexts where editing
|
||
is enabled (see §8.3).
|
||
|
||
The center column's breadcrumb reads, for example: `OHM › main ▾ · 3
|
||
branches · 1 PR`. The chevron opens a dropdown listing `main` at the
|
||
top, then open branches sorted by recent activity (with a visibility
|
||
indicator for any private ones), then open PRs with their status, and
|
||
a "Show closed branches" toggle at the bottom. Selecting any item
|
||
swaps both the document body and the right-pane chat thread together.
|
||
The URL updates accordingly (`/rfc/<slug>/branches/<name>`), so a
|
||
branch view is shareable and back/forward navigation works.
|
||
|
||
### 8.2 Default view on selection
|
||
|
||
Selecting an active RFC opens the center column on the RFC's `main`
|
||
body, rendered in the editor in read-only mode (the discuss-mode
|
||
default — see §8.3). The right column shows main's chat thread. The
|
||
breadcrumb indicates `main`. Selecting a different branch via the
|
||
dropdown swaps the body to that branch's `RFC.md` and the chat to
|
||
that branch's thread.
|
||
|
||
### 8.3 Discuss vs. contribute mode (branch-scoped)
|
||
|
||
The prototype's discuss-vs-contribute distinction survives, but is
|
||
scoped to the current branch rather than global.
|
||
|
||
- **Discuss mode** is the default on any branch (including main).
|
||
The editor is read-only. The chat is enabled (subject to §6's
|
||
role rules and §11.4's visibility rules). AI-proposed changes are
|
||
*buffered* — they appear in chat but do not apply to the document —
|
||
and a contribution CTA surfaces in the right column when buffered
|
||
changes exist.
|
||
- **Contribute mode** flips a single branch into edit-enabled. The
|
||
editor becomes editable. AI changes apply to the change-card panel.
|
||
Manual edits are tracked. The mode is reversible; the user can
|
||
return to discuss mode on the same branch without losing state.
|
||
|
||
On main, contribute mode is not available directly — main is read-only
|
||
by definition (PRs are the only path to change main). The "Start
|
||
Contributing" button on main instead creates a new branch and lands
|
||
the user on it in contribute mode. New-branch naming defaults to an
|
||
auto-generated value (user-renamable); the exact format is an
|
||
implementation detail.
|
||
|
||
Discuss vs. contribute is an *intent* affordance, not a *permission*
|
||
affordance. A user without contribute access to a branch sees the
|
||
toggle disabled, with a sign-in or request-access path (see §8.7).
|
||
|
||
### 8.4 Chat threading: per-branch with lineage by link
|
||
|
||
Each branch — including main — has its own chat thread. Threads are
|
||
not shared across branches.
|
||
|
||
- Main's chat is about the RFC overall.
|
||
- Each branch's chat is about that branch's work.
|
||
- A branch chat starts empty. The branch creation event records
|
||
the main-chat message that motivated it (when applicable), and
|
||
the branch chat header surfaces a "Forked from this conversation
|
||
→" link to that message. The link is a UI affordance, not a chat
|
||
message — it does not appear in transcripts.
|
||
- On PR merge, the branch chat persists attached to the (now-closed)
|
||
branch as historical record. It does not merge into main's chat.
|
||
|
||
Chat visibility follows the branch's read visibility (§11.1). Posting
|
||
requires contributor role and (for branches in non-public contribute
|
||
modes) the relevant grant per §6.4.
|
||
|
||
### 8.5 AI conversation continuity into PRs
|
||
|
||
When a branch becomes a PR, the PR review view surfaces the diff and
|
||
the branch's chat thread together — alongside, or stacked, per layout
|
||
constraints. The chat is shown in *compressed* form by default:
|
||
|
||
- Messages that produced accepted changes (and their immediately
|
||
surrounding context) are expanded.
|
||
- The rest is collapsed behind a "Show full conversation" toggle.
|
||
|
||
This treats the conversation as first-class evidence (per the
|
||
framework's philosophy — see `PHILOSOPHY.md`) without overwhelming
|
||
reviewers with exploratory back-and-forth. The exact rendering of the
|
||
compressed view is an implementation detail; the binding part is that
|
||
the conversation is the default surface, not a sidequest.
|
||
|
||
### 8.6 Tracked changes → commits
|
||
|
||
Each accepted AI change becomes one commit on the branch immediately.
|
||
The commit message body contains the change's `original`, `proposed`,
|
||
and `reason`. Trailers carry the conversation message ID that produced
|
||
the change and the standard `On-behalf-of:` per §6.5.
|
||
|
||
Manual edits buffer locally in the editor. They flush as one commit
|
||
on:
|
||
|
||
- A short idle window after the user stops typing (default ~5 min;
|
||
exact value is an implementation detail).
|
||
- Branch switch.
|
||
- Explicit save gesture.
|
||
|
||
Every commit pushes immediately to Gitea. There is no local-vs-remote
|
||
split — the branch on Gitea is the canonical state. Collaborators on
|
||
a shared branch see each other's commits as they land.
|
||
|
||
### 8.7 Read-only fallbacks
|
||
|
||
Anonymous users and muted contributors (per §6.1, §6.2) can read but
|
||
not write or chat. Their experience in the RFC view:
|
||
|
||
- **Document.** Renders as in the standard view. The editor is
|
||
read-only regardless of mode.
|
||
- **Chat thread.** Visible per §11.4. Chat input is shown but
|
||
disabled, with a "Sign in to chat" button (anonymous) or a muting
|
||
banner ("Your account is muted. Contact an admin." — muted).
|
||
- **Selection tooltip.** Highlighting still works; the submit action
|
||
reads "Sign in to ask" or is disabled with a muting hint.
|
||
- **Header.** "Start Contributing" is replaced with "Sign in" for
|
||
anonymous users; for muted contributors, the button is disabled
|
||
with a tooltip explaining why.
|
||
- **Breadcrumb dropdown.** Lists only the branches the user can read
|
||
per §11.1. Muted contributors retain their own existing branches in
|
||
the listing.
|
||
|
||
The intent is to keep the invitation visible while making it
|
||
impossible to act without the appropriate role.
|
||
|
||
### 8.8 The change-card panel
|
||
|
||
The change-card panel is the right-column surface where the branch's
|
||
proposed and acted-on changes accumulate. In contribute mode it
|
||
renders below the chat as a persistent second pane — chat above,
|
||
changes below — with pending cards stacked on top of resolved ones
|
||
inside the panel. In discuss mode the panel is hidden; a single
|
||
contribution-CTA card surfaces in its place per §8.14.
|
||
|
||
The chat and the panel are bound bidirectionally. Each assistant
|
||
message that produced changes carries a clickable "↓ N changes added
|
||
below" hint that scrolls the panel to those cards and flashes them.
|
||
Each card carries an "↑ from this message" affordance that scrolls
|
||
the chat back to the originating message and highlights its bubble.
|
||
The conversation is the evidence the change was earned; the
|
||
click-binding keeps both halves of that evidence reachable from
|
||
either side without depending on the contributor remembering where
|
||
in the timeline they're standing.
|
||
|
||
A pending card carries the proposed change's diff (inline word-level
|
||
for both AI and manual), the originating author label, and the
|
||
accept/decline/edit affordances specified in §8.9. A resolved card
|
||
collapses to a compact one-line stub — author · state · short reason —
|
||
that stays in the panel as evidence. Resolved cards never return to
|
||
pending; revising a previously-declined or previously-accepted
|
||
decision means producing a new card from a new conversation turn.
|
||
|
||
### 8.9 Accept, decline, and edit
|
||
|
||
Three actions resolve a pending card. **Accept** runs a ProseMirror
|
||
transaction that locates the change's `original` text in the editor as
|
||
a range, deletes it, inserts the `proposed` text in its place, and
|
||
wraps both insertion and deletion in tracked-change marks carrying
|
||
the change's ID. The commit fires immediately per §8.6 and the card
|
||
moves to its resolved stub form. The fallback for ambiguous ranges
|
||
— the `original` text appearing in more than one place in the
|
||
document — is to refuse the automatic apply and surface a "this
|
||
change can't be auto-applied; review and accept manually" state on
|
||
the card.
|
||
|
||
**Edit-before-accept** is one card with a `was_edited_before_accept`
|
||
flag (§5), not two. The card opens an in-place textarea pre-filled
|
||
with `proposed`; saving updates `proposed` to the contributor's
|
||
revision and flips the flag. The subsequent accept produces a single
|
||
commit; the commit body carries both the AI's original proposed text
|
||
under an `AI proposed:` section and the contributor's accepted
|
||
revision in the usual `proposed` body, so the timeline preserves what
|
||
was offered and what landed as distinct artifacts. The commit's
|
||
`On-behalf-of:` trailer per §6.5 names the contributor, not the AI;
|
||
the AI's authorship survives only in the `AI proposed:` body section
|
||
and in the `source_message_id` linkage.
|
||
|
||
**Decline** is not a commit — no document state changed — but the
|
||
card persists. The card moves to its resolved stub form
|
||
(author · declined · the AI's short reason) and stays in the panel
|
||
and the `changes` table with `state='declined'`. There is no undo
|
||
affordance. The framework's claim depends on refusal being legible:
|
||
a contributor scrolling the resolved list should see what they
|
||
considered and rejected, not just what they accepted. To re-propose,
|
||
ask the AI again — the new proposal lands as a new card with the
|
||
declined predecessor still visible.
|
||
|
||
### 8.10 Tracked-change markup and the review-mode toggle
|
||
|
||
Two visual layers carry change information in the editor. The first
|
||
is a paragraph-margin marker — a thin gutter accent on any paragraph
|
||
that differs from the branch's open-session baseline, rendered by a
|
||
ProseMirror plugin against a baseline snapshot taken when the editor
|
||
opens. The second is inline `tracked-delete` / `tracked-insert`
|
||
markup at the exact range of an accepted change, with the deleted
|
||
text shown struck-through and the inserted text shown in an additive
|
||
style; both carry the change's ID via a data attribute, enabling the
|
||
click-to-card binding from §8.8. The margin marker is scannable
|
||
("did anything change in this region?"); the inline markup is
|
||
precise ("what changed here?"). The two answer different questions
|
||
and both are kept.
|
||
|
||
The inline markup is session-local. Each accepted change is already
|
||
a clean commit on Gitea per §8.6, so the branch's canonical state at
|
||
any reload is the integrated text without markup. Regenerating
|
||
markup on load by diffing against earlier commits is technically
|
||
possible but adds a mechanism — a per-user seen-cursor for accepted
|
||
changes, an explicit dismiss UI — to solve a problem that DiffView
|
||
already solves durably and at higher fidelity. The editor is for
|
||
writing; layering permanent diff overlay on top of writable text
|
||
degrades the writing surface, so the markup clears on reload and
|
||
DiffView is the durable artifact for inspecting accepted changes in
|
||
context.
|
||
|
||
DiffView is the read-only render surface invoked via a toolbar
|
||
toggle, carried over from the prototype per §8.15. It reads from the
|
||
`changes` table for the branch, reconstructs the markup for every
|
||
accepted change in branch history, and renders the result in-place
|
||
where the editor was. Hovering any marked span surfaces a tooltip
|
||
with the change's type badge (`ai` or `manual`), the model
|
||
identifier where applicable, the `was_edited_before_accept` flag
|
||
where set, the user prompt and selection-quote that drove the
|
||
change, and the AI's `reason`. The toggle is reversible; returning
|
||
to the editor restores the live writing surface and reattaches the
|
||
session-local baseline.
|
||
|
||
### 8.11 Manual edits and collisions with AI proposals
|
||
|
||
Manual edits accumulate in the editor and surface in the change-card
|
||
panel as a single pending card per flush window, growing one inline
|
||
word-diff per touched paragraph as the contributor types (debounced
|
||
on the order of a second — exact value is an implementation detail).
|
||
The card carries a quiet status line — `unsaved · auto-save in 4:32`
|
||
— that counts down toward the §8.6 idle threshold, plus an explicit
|
||
`Save now` button that flushes immediately. On flush — idle, branch
|
||
switch, or save — the card freezes into its resolved stub form with
|
||
the commit SHA from §8.6 attached, and a fresh pending manual card
|
||
opens on the next keystroke. The pending card's diffs and the
|
||
resolved card's diffs each match exactly one Git commit, preserving
|
||
the §8.6 evidence-unit framing.
|
||
|
||
When an AI proposal's `original` text can no longer be located in
|
||
the current document because a manual edit has changed it since the
|
||
proposal was generated, the AI's card is marked **stale** — the
|
||
`stale_since` timestamp on the `changes` row is set, distinct from
|
||
`state`, which stays `pending` until the contributor acts. The card
|
||
surfaces a warning badge and a `Re-ask` button that re-prompts the
|
||
AI with the current text to regenerate the proposal anchored to the
|
||
new phrasing. Accept is gated behind a confirmation step on a stale
|
||
card — "text has changed since this was proposed; apply anyway?" —
|
||
for the case where the contributor judges the AI's proposal still
|
||
applicable despite the drift. Silent re-merging is refused: the
|
||
AI's argument was about a specific phrasing, and applying it to
|
||
different phrasing produces text neither party authored.
|
||
|
||
### 8.12 Threads on a branch: anchors and the chat surface
|
||
|
||
Every branch has a default `whole-doc` thread — the branch chat
|
||
referenced throughout §8 and §10. Beyond that default, sub-threads can
|
||
be anchored to a range or a paragraph (`anchor_kind` per §5) and
|
||
accumulate inside the same chat surface. The branch chat is the
|
||
unified chronological feed of every message across every thread on
|
||
the branch; threads are the structural grouping inside that feed,
|
||
not a separate surface.
|
||
|
||
Range threads are created by the selection tooltip carried over per
|
||
§8.15. Submitting a tooltip prompt creates a new `thread_kind='chat'`,
|
||
`anchor_kind='range'` thread anchored to the selection, or continues
|
||
an existing open thread whose anchor overlaps the new selection
|
||
substantially — a threshold (default in the neighborhood of 50% of
|
||
characters) prevents one passage from spawning a forest of single-Q&A
|
||
orphans while still allowing a deliberately different framing to
|
||
start a new conversation. Paragraph threads are created via a
|
||
margin-icon affordance that appears on hover beside any paragraph;
|
||
clicking opens a thread-kind picker (chat or flag — flags per §8.13)
|
||
and a composer for the first message.
|
||
|
||
In the feed, sub-threads render with a thin colored gutter and a
|
||
small anchor preview ("quoted: …"), grouping their messages visually
|
||
without breaking chronology. A top-of-chat disclosure shows aggregate
|
||
counts — `N open threads · M open flags` — and expands to a list of
|
||
open threads with anchor previews and per-thread filter affordances
|
||
that collapse the feed down to a single thread. Clicking the anchor
|
||
preview on any message scrolls the editor to that anchor and
|
||
highlights the range or paragraph. A `Reply` affordance on any
|
||
message posts back into its thread; the AI participant can be
|
||
invoked into any thread, not only the whole-doc default, and its
|
||
`<change>` proposals carry the `thread_id` they were generated
|
||
within.
|
||
|
||
A thread is resolved by its creator, by the branch creator, by any
|
||
of the RFC's owners or arbiters per §6.3, or by app-wide admins or
|
||
owners. Resolution writes `state='resolved'` with `resolved_by` and
|
||
`resolved_at`; in the chat feed, resolved threads collapse to a
|
||
one-line stub (expandable) and any editor anchor markers fade to
|
||
nearly invisible. Threads auto-transition to `state='stale'` when
|
||
their anchor no longer maps to the document — the paragraph deleted,
|
||
the range no longer locatable after edits — and the editor anchor
|
||
surface is replaced with an "(anchor lost)" affordance that disables
|
||
the scroll-to-editor binding. Resolved and stale threads stay in the
|
||
data; the chat feed's filter affordances surface or hide them on
|
||
demand.
|
||
|
||
### 8.13 Flags
|
||
|
||
A flag is the lightweight "I'm pointing at this, it's a problem"
|
||
gesture — a single declarative assertion that something is wrong,
|
||
incomplete, or unclear, anchored to a range, a paragraph, or the
|
||
document as a whole. The flag's entire content lives in
|
||
`threads.label` (§5); no back-and-forth is required to drop one. The
|
||
schema is shared with chat threads — `thread_kind='flag'` — so the
|
||
surfacing, resolution, and stale mechanics from §8.12 apply
|
||
unchanged. The distinction is gestural and visual, not structural.
|
||
|
||
Creation requires contributor role but not contribute access to the
|
||
branch: any signed-in contributor who can read a passage can point at
|
||
it and say it's wrong. Flags are created via the same two affordances
|
||
as chat sub-threads — the margin-icon picker on a paragraph, or the
|
||
selection tooltip's `Flag` button alongside its prompt input — with a
|
||
short text input (capped on the order of 200 characters; exact value
|
||
is implementation detail) for the flag content. In the editor, flag
|
||
anchors render with a flag icon at the anchor in a color distinct
|
||
from the chat-thread margin icon. In the chat feed, flags appear
|
||
inline chronologically with a flag badge. In the top-of-chat
|
||
disclosure, flags are counted separately from chat threads.
|
||
|
||
A flag can convert to a chat by anyone replying to it; the schema
|
||
accommodates this directly — `thread_messages` rows accumulate
|
||
against the flag's thread, and `thread_kind` stays `flag` as the
|
||
lineage marker. The AI participant can be invoked on a flag via an
|
||
`Ask Claude to propose a fix` button on the flag in the right pane,
|
||
which generates a `<change>` proposal anchored to the flag's thread
|
||
and implicitly converts the flag to a chat, since the AI's response
|
||
is the first reply. Accepting a `<change>` proposal generated from a
|
||
flag does *not* auto-resolve the flag — the flag-creator's claim may
|
||
have been broader than the specific change addressed, and the
|
||
explicit resolution gesture from §8.12 stays the only way to mark
|
||
the flag closed.
|
||
|
||
Flags do not block PR merge per §10.5's principle — making them a
|
||
merge gate would re-create the "resolved to unblock" failure mode
|
||
the §10.5 rationale already refuses for review threads. But they are
|
||
prominent: the PR header from §10.3 surfaces the open-flag count
|
||
alongside the open-review-thread count, and the count is sized to
|
||
register on a reviewer's scan.
|
||
|
||
### 8.14 Discuss-mode buffered proposals
|
||
|
||
Per §8.3, discuss mode buffers AI-proposed changes rather than
|
||
applying them. The buffer is not a separate data store: every
|
||
`<change>` block parsed from an assistant message becomes a `changes`
|
||
row with `state='pending'` immediately, regardless of mode. The mode
|
||
determines only what the UI renders. In contribute mode the panel
|
||
from §8.8 shows the pending rows as full cards; in discuss mode the
|
||
panel is hidden and a single contribution-CTA card surfaces in its
|
||
place, listing the count of pending proposals and offering a `Preview
|
||
proposed changes` disclosure that reveals the cards in read-only
|
||
form. The primary action on the CTA is `Start Contributing →`.
|
||
|
||
What that action does depends on which branch the contributor is on.
|
||
On a non-main branch, `Start Contributing` is a pure mode flip — the
|
||
contributor is already on the branch, the pending rows are already
|
||
anchored to it, and surfacing the action affordances is the only
|
||
state change. On main, where contribute mode is unavailable per
|
||
§8.3, `Start Contributing` cuts a new branch from main's tip,
|
||
re-anchors the pending rows by mutating their `branch_name` from
|
||
`main` to the new branch's name — the changes haven't been acted on
|
||
yet, so there's no audit trail to corrupt — and navigates the
|
||
contributor to the new branch in contribute mode. The pending rows'
|
||
`source_message_id` continues to reference messages in main's chat;
|
||
the schema permits the cross-branch reference and the UI labels it
|
||
as `from a conversation on main`, while the `Forked from this
|
||
conversation →` link in the new branch's chat header from §8.4
|
||
closes the loop in the opposite direction.
|
||
|
||
Toggling contribute back to discuss on a non-main branch hides the
|
||
panel without touching data. Re-flipping to contribute resurfaces
|
||
the panel identically. The buffer-vs-applied distinction is purely
|
||
surface — under the data model, every AI proposal is a first-class
|
||
artifact from the moment it lands, on whatever branch the
|
||
conversation produced it on.
|
||
|
||
### 8.15 Carryovers and implementation-deferred details
|
||
|
||
These prototype affordances carry over with branch-scoped behavior:
|
||
the selection tooltip (elaborated in §8.12 as the range-thread
|
||
entry point); the review-mode toggle and `DiffView` for inspecting
|
||
accepted changes in context (§8.10); the discuss-mode banner
|
||
indicating read-only status; the `<change>` / `<original>` /
|
||
`<proposed>` / `<reason>` AI protocol (§18).
|
||
|
||
The following are implementation-level details that the build session
|
||
will decide:
|
||
|
||
- New-branch naming format on the "Start Contributing from main"
|
||
flow (§8.14).
|
||
- Exact idle-window length for the manual-edit commit flush (§8.11).
|
||
- The compressed-conversation rendering in PR review (§8.5).
|
||
- The exact overlap threshold for continue-vs-create-new on
|
||
range-anchored threads (§8.12).
|
||
- Stable paragraph-ID machinery for paragraph-anchored thread
|
||
payloads (§5's `anchor_payload`).
|
||
- The 200-character cap on flag content (§8.13).
|
||
|
||
Super-draft document-pane behavior, the propose flow, and editing a
|
||
super-draft body are covered in §9 — a sibling section that maps the
|
||
same machinery onto the super-draft surface, with meta-repo edit
|
||
branches as the unit of work in place of per-RFC branches.
|
||
|
||
---
|
||
|
||
## 9. The super-draft view and lifecycle
|
||
|
||
The catalog (§7) settles the left-pane treatment of super-drafts; the
|
||
active-RFC view (§8) settles the surface a graduated RFC is read and
|
||
edited on; the graduation flow (§13) settles the bridge between
|
||
super-draft and active. This section settles the middle of that arc:
|
||
how an idea enters the catalog at all, what a super-draft looks like
|
||
once it does, and how its body and conversation evolve up to the
|
||
graduation moment.
|
||
|
||
The framing claim from PHILOSOPHY.md does most of the structural work
|
||
here. Super-drafts are the moment a word enters the conversation;
|
||
most proposals will not survive the argument, and that is fine. The
|
||
super-draft view is where survival or non-survival gets argued out —
|
||
which means the argument tooling has to be the same tooling §8 spent
|
||
its length specifying. Stripping the revision flow on super-drafts
|
||
would strip it from the phase where definitions are most generative.
|
||
|
||
### 9.1 Proposing a new RFC
|
||
|
||
The "+ Propose new RFC" affordance in the left pane (§7.3) opens the
|
||
**propose modal**. Available to any contributor; anonymous viewers
|
||
see the affordance replaced with a sign-in CTA per §8.7's pattern.
|
||
|
||
The modal collects four fields:
|
||
|
||
- **Title** — required short text input. The word or topic this RFC
|
||
would define. Contributor-typed; title-first matches the natural
|
||
cognitive flow, since a proposer arrives with a word in mind, not
|
||
with a pitch in search of a word.
|
||
- **Slug** — deterministically kebab-cased from the title as the
|
||
contributor types, inline-editable. Validated for uniqueness
|
||
against `rfcs/` on the meta-repo main *and* against the slugs of
|
||
any open idea PRs, so concurrent proposers cannot collide. A
|
||
collision surfaces inline ("`open-human-model` is taken — try
|
||
`open-human-model-2`?"). The API re-checks atomically at submit.
|
||
- **Pitch** — required textarea. One or two paragraphs answering
|
||
"why this RFC is needed," contributor-typed. Becomes the entry
|
||
file's body per §2.1.
|
||
- **Tags** — optional chip input with AI-suggested chips populating
|
||
after a debounce on the pitch. Contributor can accept, dismiss, or
|
||
type their own.
|
||
|
||
No proposer name or email — the logged-in identity is canonical and
|
||
need not be retyped. No proposed-owner or working-group fields —
|
||
ownership flows through the post-merge claim flow (§13.1), and
|
||
arbiters are admin work, not the proposer's call. AI's drafting role
|
||
is intentionally narrow: tag suggestions only. The proposer is
|
||
making a specific claim about a specific word, and having AI propose
|
||
the claim for them would undercut the gesture.
|
||
|
||
Primary action: **"Open proposal PR"** — naming the actual Git
|
||
artifact produced, consistent with §10.1's *Open PR* and §13's
|
||
*Graduate*.
|
||
|
||
### 9.2 The proposal PR
|
||
|
||
Submitting the modal opens a PR against the meta repo per §2.2,
|
||
adding exactly one file at `rfcs/<slug>.md`. The file's frontmatter
|
||
is populated from the modal and session:
|
||
|
||
- `slug`, `title`, `tags` — from the modal.
|
||
- `state: super-draft`, `id: null`, `repo: null`,
|
||
`graduated_at: null`, `graduated_by: null` — fixed at creation.
|
||
- `proposed_by: <session email>`, `proposed_at: <today>` — auto.
|
||
- `owners: []` — empty; the claim flow (§13.1) fills this.
|
||
- `arbiters: []` — empty; arbiters are admin work, not the
|
||
proposer's call.
|
||
|
||
The file's body is the pitch as typed. The bot is the author per §1;
|
||
the standard `On-behalf-of:` trailer per §6.5 names the proposer.
|
||
|
||
The PR title and the file-add commit subject share a fixed pattern:
|
||
**`Propose: <Title>`**. Mechanical, scannable — an owner viewing the
|
||
meta-repo's PR list sees "Propose: Open Human Model," "Propose:
|
||
Trait," "Propose: Consent" and triages at a glance.
|
||
|
||
The PR description is AI-drafted from the pitch — two or three
|
||
sentences in spec voice making the *case for catalog admission*,
|
||
distinct from the pitch body itself. The admission case answers: is
|
||
this in scope, does it overlap an existing entry, is it the right
|
||
shape for a single RFC. Audience is an owner or admin, per §13.1's
|
||
framing for the merge actor. Editable inline in the modal before
|
||
submit, and editable post-open on the PR by the proposer or any
|
||
owner/admin. This is the propose-flow analogue of §10.2's
|
||
AI-drafted-from-chat description — different source material, same
|
||
role.
|
||
|
||
No reviewer picker. Meta-repo owners and admins are the implicit
|
||
reviewer set, same logic as §10.2.
|
||
|
||
### 9.3 The pending-idea view
|
||
|
||
Submitting the modal closes it and navigates the proposer to the
|
||
**pending-idea view** — the in-app read surface for any open idea
|
||
PR, available to anyone per §7.3's "Reading is open to all." The
|
||
left pane's pending-ideas disclosure expands and highlights the new
|
||
entry. There is no success interstitial; the navigation *is* the
|
||
success state, consistent with §1's claim that the app is the
|
||
contribution surface.
|
||
|
||
The pending-idea view renders the proposed entry's body and
|
||
frontmatter (read-only) using the same renderer §9.4 specifies for
|
||
super-drafts proper. A status banner reads "Pending idea — awaiting
|
||
review." The header strip carries:
|
||
|
||
- **Withdraw proposal** — proposer-only. Closes the meta-repo PR.
|
||
The view enters a "Withdrawn" terminal read-only state, mirroring
|
||
§10.8's PR treatment.
|
||
- **View on Gitea** — unobtrusive footer link. The power-user
|
||
escape hatch.
|
||
- **Merge proposal** — visible to owners and admins per §6.1.
|
||
Merging is what creates the super-draft entry on meta-repo main.
|
||
- **Decline** — visible to owners and admins. Opens a two-step
|
||
dialog: first a comment composer (required textarea), then a
|
||
preview-and-confirm step rendering exactly what the proposer will
|
||
read — decliner display name, decline date, the comment verbatim.
|
||
The admin can edit the comment from the preview or confirm to
|
||
send. Confirming closes the meta-repo PR with the comment recorded
|
||
both as a meta-repo PR comment (the durable Git artifact) and as a
|
||
system-author message in the pending-idea chat thread ("Declined
|
||
by @admin: <comment>"), so the chat record per the rules in this
|
||
section carries the act inline. The preview step is the
|
||
ceremony — decline is the sister gesture to graduation, and seeing
|
||
what the proposer will read before sending makes the cost of the
|
||
act concrete.
|
||
|
||
A chat thread accumulates on the pending-idea view pre-merge —
|
||
contributors can argue about whether the entry belongs in the
|
||
catalog before it is admitted. On merge, that chat migrates to the
|
||
super-draft's main chat per the same principle §13.4 commits for
|
||
graduation: chat follows the work. The data path is straightforward.
|
||
The threads carry `rfc_slug = <proposed slug>` pre-merge (slugs are
|
||
reserved during the idea PR per §9.1's uniqueness check), and on
|
||
merge they simply surface under the super-draft's main view with no
|
||
data movement. On decline, the threads stay attached to the closed
|
||
PR as historical record and do not surface in any default view, the
|
||
same treatment a withdrawn entry receives.
|
||
|
||
Outcome signals to the proposer are narrow. On next visit after
|
||
merge or decline, the pending-idea view is either the super-draft
|
||
view itself (merged) or a read-only "Declined" banner (declined),
|
||
and a one-time in-app toast surfaces — "Your proposal *Open Human
|
||
Model* was merged. It's now a super-draft." or "Your proposal *Open
|
||
Human Model* was declined." The "Declined" banner carries the
|
||
decliner's display name, the decline date, and the comment from the
|
||
two-step dialog verbatim, rendered above the proposal body and
|
||
frontmatter (which remain readable below for context). Beneath the
|
||
banner, a small "Propose a revised entry" affordance opens the
|
||
propose modal pre-filled from the prior proposal's title, slug, and
|
||
pitch — the philosophy frames non-survival of a proposal as
|
||
expected, and a one-step path back to the modal honors a proposer
|
||
who has absorbed the decline reason. Beyond the toast and the
|
||
banner, the proposal-merged and proposal-declined events also fire
|
||
into the proposer's inbox as personal-direct notifications per §15.4
|
||
(default-on email category), giving out-of-session reach for an
|
||
event whose timing the proposer cannot anticipate.
|
||
|
||
### 9.4 The super-draft view
|
||
|
||
Once the idea PR merges, the entry exists as a super-draft per §3,
|
||
and the catalog renders it as a `super-draft` row per §7.2.
|
||
Selecting it opens the **super-draft view** — a sibling surface to
|
||
§8's active-RFC view, sharing the three-column layout, the
|
||
breadcrumb, and all of §8's center-pane mechanics.
|
||
|
||
The differences are scoped. There is no integer ID in the breadcrumb
|
||
per §2.3. The breadcrumb dropdown lists open meta-repo edit branches
|
||
(per §9.5) in the place an active RFC lists its branches, and the
|
||
dropdown's first position is **"canonical body"** — the entry as it
|
||
appears on meta-repo main — in the place an active RFC's main sits.
|
||
There is no PR flow scoped to *this* RFC's own repo (it has none
|
||
yet); there can be open body-edit PRs against the meta repo, surfaced
|
||
inline with the edit branches in the dropdown.
|
||
|
||
The document pane uses the same Tiptap editor as active RFCs, in
|
||
read-only mode by default, identical to §8.2. The toolbar shape, the
|
||
selection-tooltip from §8.12, the §8.13 flag affordances, and the
|
||
review-mode/DiffView toggle from §8.10 all map across. The discuss
|
||
vs. contribute mode toggle from §8.3 exists; the canonical body is
|
||
in discuss mode like main on an active RFC, and the "Start
|
||
Contributing" affordance cuts an edit branch per §9.5 rather than
|
||
flipping mode inline.
|
||
|
||
§8.7's read-only fallbacks apply identically. Anonymous viewers see
|
||
the full body, the selection tooltip works but its submit is
|
||
disabled, the chat input renders disabled with a "Sign in to chat"
|
||
hint, and the "Start Contributing" affordance is replaced with
|
||
"Sign in." Muted contributors see the same surface with a muting
|
||
banner per §6.2.
|
||
|
||
### 9.5 Editing a super-draft body
|
||
|
||
Super-drafts have no per-RFC repo, so they have no branches in the
|
||
§8 sense. Edits to the body propagate through PRs against the meta
|
||
repo per §2.2's framing. To carry the §8 revision flow onto this
|
||
target without ceremony, the unit of work is a **branch on the meta
|
||
repo, scoped to the super-draft's edit**.
|
||
|
||
The super-draft view's center column behaves like an active RFC's
|
||
`main` view per §8.3: read-only by default, with a "Start
|
||
Contributing" CTA. Activating it cuts a fresh branch off the meta
|
||
repo's `main`, naming it `edit/<slug>/<auto-name>` (exact format an
|
||
implementation detail), and lands the contributor on that branch in
|
||
contribute mode. The branch's only file under edit is
|
||
`rfcs/<slug>.md`; the editor never exposes the rest of the meta
|
||
repo's contents, and attempts to navigate elsewhere on the branch
|
||
fall back to the super-draft view.
|
||
|
||
On the edit branch, everything from §8.4 through §8.14 applies
|
||
unchanged. The per-branch chat (§8.4), AI proposals materializing
|
||
as `<change>` rows, accept/decline/edit-before-accept (§8.9),
|
||
manual-edit flushes (§8.6), the change-card panel (§8.8), range and
|
||
paragraph sub-threads (§8.12), flags (§8.13), DiffView (§8.10),
|
||
stale-change handling (§8.11). The "branch" abstraction §8 was
|
||
written against is the meta-repo edit branch; the machinery does
|
||
not care whether the underlying repo is per-RFC or meta.
|
||
|
||
Opening a PR is §10.1's gesture, with the meta-repo branch as
|
||
source and the meta repo as target. The PR creation modal (§10.2),
|
||
the review page (§10.3), the review-comment surface (§10.4), and
|
||
the seen-cursor mechanism from §10.3 all apply unchanged. The merge
|
||
actor set is the **super-draft's owners and arbiters per §6.3,
|
||
plus app-wide admins/owners per §6.1**. An unclaimed super-draft has
|
||
no owners, so until the claim flow (§13.1) runs, only app-wide
|
||
admins/owners can merge body-edit PRs — sensible because an
|
||
unclaimed super-draft is by definition awaiting an owner, and
|
||
admin oversight is the only path to canonicalizing edits in the
|
||
interim.
|
||
|
||
Multiple body-edit branches can coexist on the same super-draft. The
|
||
super-draft view's breadcrumb dropdown lists them exactly the way
|
||
an active RFC's lists its branches. Merge conflicts invoke §10.9's
|
||
resolution-branch replay path against the meta repo.
|
||
|
||
Frontmatter edits — title and tags — are out of scope for the
|
||
editor's in-line edit surface in v1. A small **metadata pane** on
|
||
the super-draft view permits title and tag edits; each edit produces
|
||
a tiny meta-repo PR via the bot, distinct from a body-edit branch.
|
||
Slug renames are not supported in v1 — a slug rename is a file
|
||
rename in Git plus a cache-key rewrite, rare enough to defer to a
|
||
future topic (see §19.2). The proposer's slug choice is canonical
|
||
until graduation, at which point the integer ID becomes the stable
|
||
handle per §13.2.
|
||
|
||
### 9.6 Chat and threads on a super-draft
|
||
|
||
The chat and thread model from §8.4, §8.12, and §8.13 extends to
|
||
super-drafts in the natural way:
|
||
|
||
- The super-draft has a **main chat** — the whole-doc default
|
||
thread, stored as a `threads` row with the super-draft's slug as
|
||
`rfc_slug` and `branch_name = null`, identical in shape to an
|
||
active RFC's main thread per §8.4. This is the durable
|
||
conversation surface about the canonical body as it appears on
|
||
meta-repo main.
|
||
- **Each meta-repo edit branch has its own chat** per §8.4, with
|
||
`branch_name` set to the edit branch's name. On merge or close,
|
||
the chat persists attached to the (now-closed) branch as
|
||
historical record, exactly per §8.4's rule. After §12's 90-day
|
||
deletion timer fires, the metadata row remains in the cache for
|
||
historical reference and the chat data remains in the app
|
||
database.
|
||
- **Range and paragraph sub-threads (§8.12) and flags (§8.13)**
|
||
apply on both the super-draft's main view and on edit branches.
|
||
The AI participant is invocable on any thread. The
|
||
selection-tooltip and margin-icon affordances from §8.12 work
|
||
identically.
|
||
- **Anchor stability.** §8.12's stale mechanic is general enough to
|
||
cover both cases. On an edit branch, anchors churn
|
||
commit-to-commit, same as on an active-RFC branch. On the
|
||
super-draft's main view, anchors churn only at body-edit-PR merge
|
||
boundaries — same volatility as on an active RFC's main. Anchors
|
||
that fail to relocate get the "(anchor lost)" affordance from
|
||
§8.12.
|
||
|
||
§5's existing `threads` schema accommodates all of this without
|
||
column changes. The interpretive rule recorded in §5 is: when the
|
||
entry referenced by `rfc_slug` is in state `super-draft`,
|
||
`branch_name` names a branch on the meta repo rather than on a
|
||
per-RFC repo. The `changes` table inherits the same rule.
|
||
|
||
### 9.7 Visibility and contribute on a super-draft
|
||
|
||
§11 specifies visibility (read) and contribute (push) for branches
|
||
on active-RFC repos. Because §9.5 maps super-draft body edits onto
|
||
meta-repo edit branches, §11 transfers in place with two
|
||
super-draft-specific clarifications:
|
||
|
||
- **The super-draft's canonical body is always publicly readable.**
|
||
It lives on the meta repo's main branch, which is public by
|
||
definition (§2, §14.2). There is no `read_public` toggle at the
|
||
super-draft level — nothing to hide, nothing to flip. The
|
||
pending-idea view (§9.3) is publicly readable for the same reason:
|
||
meta-repo PRs are public.
|
||
- **Edit branches inherit §11 in full.** Default
|
||
`read_public = true`, default `contribute_mode = just-me`,
|
||
per-branch contribute grants per §6.4, §11.3's PR-becomes-public
|
||
rule, §11.4's chat-inherits-from-branch rule, §12's 30/90 hygiene
|
||
timers. The set of people who can flip an edit branch's settings
|
||
is the branch creator, the super-draft's owners/arbiters (where
|
||
any have been claimed), and app-wide admins/owners. Until the
|
||
claim flow (§13.1) runs, that set is the branch creator and
|
||
app-wide admins/owners only — falling out of §6.3's strict
|
||
super-set framing without amendment.
|
||
|
||
"Contribute" as a super-draft-scope verb resolves to the concrete
|
||
set of gestures: chatting on the super-draft's main thread, opening
|
||
an edit branch via "Start Contributing," posting in an edit branch's
|
||
chat (subject to per-branch grants), claiming ownership per §13.1,
|
||
dropping a flag per §8.13, and posting on an open idea PR's
|
||
discussion pre-merge. All require contributor role per §6.1; none
|
||
requires more than that on the super-draft itself, except per-branch
|
||
grants on a `just-me` or `specific` edit branch.
|
||
|
||
The super-draft's main chat is publicly readable; posting requires
|
||
contributor — identical to an active RFC's main chat per §11.4 plus
|
||
§6.1. Anonymous viewers see the same read-only fallback per §8.7.
|
||
|
||
### 9.8 Graduation handoff additions
|
||
|
||
§13's graduation sequence was written before this section's
|
||
machinery existed. The mechanics §13 needs to absorb fold inline
|
||
into §13.2 and §13.4 in their respective sections; the substantive
|
||
additions are captured here for cross-reference:
|
||
|
||
- **Open body-edit PRs block graduation.** §13.3's step 3 removes
|
||
the meta-repo entry's body field, and an open body-edit PR
|
||
post-graduation would attempt to re-introduce a body to a
|
||
frontmatter-only entry. The Graduate dialog disables the confirm
|
||
button if any meta-repo PR is open against `rfcs/<slug>.md`. The
|
||
precondition is enforced before the bot starts §13.3's sequence,
|
||
so §13.3's rollback complexity does not grow.
|
||
- **Bare edit branches survive graduation.** Edit branches without
|
||
an open PR are not blocked. They remain on the meta repo subject
|
||
to §12's hygiene timers. The contributor can re-cut against the
|
||
new RFC repo's main if they still want the work. The branch chat
|
||
persists per §8.4 as historical record even after auto-close, so
|
||
the argument that produced the work is preserved regardless of
|
||
whether the work itself merges.
|
||
- **Chat migration includes range and paragraph sub-threads.**
|
||
§13.4's chat-follows-the-work rule covers the whole-doc main
|
||
thread; it extends to range and paragraph sub-threads on the
|
||
super-draft's main view, which migrate as part of the same
|
||
movement. Anchors re-resolve against `RFC.md` on the new repo;
|
||
since §13.3's step 2 seeds `RFC.md` from the super-draft body
|
||
verbatim, anchors typically locate the same content. Where they
|
||
do not, §8.12's stale mechanic engages.
|
||
- **Pre-graduation history surfaces from the new RFC view.**
|
||
Meta-repo edit-branch chats, flag threads, and `changes` rows
|
||
stay attached to their original `branch_name` on the meta repo;
|
||
they do not migrate. A **"Pre-graduation history"** affordance on
|
||
the new RFC view surfaces these — 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 `edit/<slug>/`. UI affordance; no data
|
||
movement, no rollback cost.
|
||
|
||
---
|
||
|
||
## 10. The PR flow
|
||
|
||
PR opening is structurally separable from doing the work. Branches
|
||
accumulate commits independently per §8.6 — one per accepted AI change,
|
||
debounced flushes for manual edits — so opening a PR is not a side-effect
|
||
of editing. It is a deliberate "ready for review" gesture against an
|
||
existing branch, and it is the surface on which the RFC's arbiters consume
|
||
the branch's evidence and decide whether to fold it into main.
|
||
|
||
### 10.1 Opening a PR
|
||
|
||
PRs open from an explicit **Open PR** affordance on the branch view. The
|
||
only hard precondition is that the branch has at least one commit ahead
|
||
of main. Any pending manual edits buffered in the editor flush
|
||
immediately at submit time — the same flush §8.6 specifies for branch
|
||
switch and explicit save. Pending AI change-cards in the branch chat —
|
||
proposed but neither accepted nor declined — remain pending. They are
|
||
chat artifacts, not document state, and they travel into the PR view as
|
||
part of the conversation surface; making them blocking would force
|
||
contributors to perform decline-busywork on every speculative suggestion
|
||
just to file for review.
|
||
|
||
If the branch is currently private (per §11.1), the submit affordance
|
||
opens a confirmation modal first — "Opening this PR will make the branch
|
||
and its history publicly readable. Continue?" — per §11.3's universal-
|
||
public rule.
|
||
|
||
### 10.2 The PR creation modal
|
||
|
||
The modal collects two fields, both AI-drafted from the diff plus the
|
||
branch chat, both editable inline before submit:
|
||
|
||
- **Title** — a one-line structural description of the change, in spec
|
||
voice. What was edited, in what way.
|
||
- **Description** — two to four sentences pulling from the chat: what
|
||
was argued, what shifted, what the contributor is asking the arbiters
|
||
to consider.
|
||
|
||
The model is told the audience is *an arbiter*, not Ben specifically —
|
||
the framework has to scale past one person. Title and description remain
|
||
editable post-open by the contributor or any of the RFC's arbiters.
|
||
|
||
There is no reviewer picker. The RFC's arbiters (§6.3) are the implicit
|
||
reviewer set; surfacing a per-PR picker would either duplicate that or
|
||
imply a notion of non-arbiter reviewers with weight that the spec
|
||
deliberately does not introduce.
|
||
|
||
### 10.3 The PR review page
|
||
|
||
The PR page inherits the three-column shape from the RFC view (§8.1).
|
||
The catalog on the left is unchanged. The center column carries the diff
|
||
in place of the editor — toggleable between unified and split views. The
|
||
right column carries the compressed conversation per §8.5, with the
|
||
review-comment surface inline below it. A header strip above the diff
|
||
carries title, editable description, status, the merge button (§10.5)
|
||
when the viewer is an arbiter or app-wide admin/owner, and aggregate
|
||
counts surfaced for reviewer scanning: open review threads (§10.4),
|
||
open chat threads on the branch (§8.12), and open flags (§8.13). The
|
||
flag count in particular is sized to register at a glance.
|
||
|
||
The conversation is rendered per §8.5's compressed default: messages
|
||
that produced accepted changes (and immediate context) expanded, the
|
||
rest behind a "Show full conversation" toggle.
|
||
|
||
**What changed since my last visit.** Each PR records a per-user
|
||
seen-cursor — the most recent commit and the most recent thread message
|
||
the viewer has seen on this PR. New diff hunks and new conversation
|
||
messages since that cursor render with a subtle accent on the next visit.
|
||
The cursor advances on view; reviewers do not have to mark anything as
|
||
read. This is what makes incremental review tractable on a PR that is
|
||
still receiving commits (§10.6).
|
||
|
||
### 10.4 Review comments
|
||
|
||
Review comments are messages in the branch chat, not a separate surface.
|
||
A reviewer selecting a range in the diff and posting a comment creates a
|
||
thread with `anchor_kind = range` — where the range refers to the post-PR
|
||
document state — and `thread_kind = review`, a new value alongside `chat`
|
||
and `flag` in §5's threads table. Review threads surface in the PR view's
|
||
right column inline with the AI conversation, visually distinguished.
|
||
Both arbiters and the original contributor (and the AI participant, on
|
||
invocation) post into review threads on equal footing; they are
|
||
resolvable per §5's existing `state` field on threads.
|
||
|
||
This is a deliberate refusal of the partition between "conversation" and
|
||
"review" that other PR surfaces draw. The framework's claim, in
|
||
`PHILOSOPHY.md`, is that the transcript of the argument is the evidence
|
||
the definition was earned. Separating review onto its own surface would
|
||
say the opposite — that review is a different kind of thing from
|
||
conversation. It isn't. The disagreement an arbiter raises about a
|
||
proposed tightening is the same kind of disagreement that produced the
|
||
proposed tightening in the first place; the only difference is its
|
||
position in the timeline.
|
||
|
||
Because review threads live in the branch chat, §8.4's persistence rule
|
||
applies to them unchanged. On merge they remain attached to the
|
||
(now-closed) branch as part of the historical record.
|
||
|
||
### 10.5 Merge
|
||
|
||
Per §6.3, the RFC's owners and arbiters can merge; per §6.1, app-wide
|
||
admins and owners retain this capability too. The PR header surfaces a
|
||
single **Merge** button to those viewers. Merging produces a
|
||
no-fast-forward merge commit on main carrying the standard
|
||
`On-behalf-of:` trailer per §6.5 and preserving the per-acceptance
|
||
commit nodes from §8.6 as individual reachable commits in main's
|
||
history. The framework's evidence claim depends on those commits
|
||
remaining inspectable — squash-merging would collapse the argument into
|
||
a single anonymous artifact and is not supported.
|
||
|
||
Merge is hard-blocked only by Git-level conflicts with main. Open
|
||
review threads, pending AI change-cards, unresolved chat threads, and
|
||
open flags (§8.13) do not block merge. Making any of those a merge
|
||
gate would re-create the GitHub failure mode where threads get hastily
|
||
"resolved" to unblock a button — which corrupts the evidence record
|
||
the framework exists to preserve.
|
||
|
||
### 10.6 Updates after open
|
||
|
||
New commits — whether from accepted AI changes on the open PR's chat
|
||
or from manual-edit flushes per §8.6 — push to the branch and
|
||
immediately surface on the open PR. The diff re-renders; new hunks and
|
||
new conversation messages are accented via the seen-cursor mechanism
|
||
from §10.3. There is no notion of review invalidation; reviews are not
|
||
discrete approval gestures (§10.5), so there is nothing to invalidate.
|
||
|
||
Manual-edit flushes additionally drop a system-author message into the
|
||
branch chat noting the flush — e.g. "manual edit: 12 lines changed in
|
||
§3.2." The conversation surface is the framework's canonical
|
||
evidence timeline, and a silent diff shift would corrupt it.
|
||
AI-accepted-change commits need no separate marker: the assistant
|
||
message whose `<change>` block produced the commit is already in the
|
||
chat, and §5's `changes.commit_sha` binds the two.
|
||
|
||
### 10.7 Post-merge
|
||
|
||
After merge, the PR page renders identically with a "Merged" banner in
|
||
the header strip. The diff, the compressed conversation, and all review
|
||
threads become read-only. The branch enters the closed state per
|
||
§12's hygiene table (the 90-day deletion timer starts; owners and
|
||
arbiters can still pin to disable it). The branch chat persists per
|
||
§8.4 as historical record, with new posts disabled. The PR page remains
|
||
at its stable URL indefinitely — it is the canonical surface for "show
|
||
me how this definition came to be."
|
||
|
||
Post-merge questions about the merged work belong on main's chat (§8.4),
|
||
not on the merged branch's. Main is the surface for ongoing argument
|
||
about the canonical document; a new branch cut from main is the path to
|
||
propose a revision.
|
||
|
||
### 10.8 Withdrawing a PR
|
||
|
||
Either the original contributor or any of the RFC's arbiters can
|
||
withdraw an open PR. Withdrawal renders the PR page read-only with a
|
||
"Withdrawn" banner — identical archive treatment to a merged PR, with a
|
||
different label. The branch is *not* deleted; it remains in its current
|
||
state, subject to §12's hygiene timers. Withdrawing a PR is not the
|
||
same gesture as withdrawing the work — the contributor may want to
|
||
return to it, or another contributor may pick it up.
|
||
|
||
### 10.9 One PR per branch; conflict resolution by replay
|
||
|
||
A branch may have at most one open PR at any time. Attempting to open a
|
||
second surfaces "This branch already has an open PR."
|
||
|
||
Merge conflicts with main are surfaced on the PR page as a read-only
|
||
banner identifying the conflicting regions and offering a **Start
|
||
resolution branch** affordance. Activating it cuts a fresh branch off
|
||
the current tip of main, replays the source branch's diff into it —
|
||
running the AI participant against unambiguous conflicts and surfacing
|
||
the rest for the contributor to resolve manually — and opens a new PR.
|
||
The resolution branch's chat is seeded with a "Forked from this
|
||
conversation →" link to the original branch's chat per §8.4, so the
|
||
argument chain remains traceable across the hop. The original PR
|
||
auto-closes when the resolution PR merges.
|
||
|
||
Conflict resolution by fixup commit on the existing branch is not
|
||
supported. Per-accepted-change commit granularity (§8.6) is the
|
||
framework's evidence unit; admitting plumbing commits — "fix merge
|
||
conflict with main" — into that timeline would dilute the signal each
|
||
commit is meant to carry.
|
||
|
||
---
|
||
|
||
## 11. Branches and PRs: visibility, contribute, lifecycle
|
||
|
||
### 11.1 Visibility (read)
|
||
|
||
A branch's `read_public` defaults to true. Anyone, including
|
||
anonymous viewers, can see the branch exists, view its diffs, and
|
||
view its associated chat. The branch creator can flip a branch to
|
||
private, which restricts read to: the creator, any explicit
|
||
grantees, and the RFC's owners and arbiters. Owners and arbiters
|
||
can flip it back.
|
||
|
||
### 11.2 Contribute (push)
|
||
|
||
Default `contribute_mode` is `just-me` (see §6.4 for the three
|
||
modes). The creator and the RFC's owners/arbiters can change this
|
||
setting at any time.
|
||
|
||
### 11.3 PRs are always fully public
|
||
|
||
Opening a PR makes the branch and its history publicly readable
|
||
regardless of prior visibility. If the branch was private, the PR
|
||
modal warns the user: "Opening this PR will make the branch and
|
||
its history publicly readable. Continue?" There is no concept of
|
||
a private PR.
|
||
|
||
### 11.4 Chat visibility inherits from the branch
|
||
|
||
The chat thread attached to a branch is readable by exactly the
|
||
people who can read the branch. Anonymous users can read branch
|
||
chat on a public branch but cannot post. Posting requires
|
||
contributor or higher.
|
||
|
||
### 11.5 Branch lifecycle hygiene
|
||
|
||
A branch with no associated PR auto-closes at 30 days from last
|
||
commit. A closed branch is deleted at 90 days. Closed branches
|
||
remain publicly readable (if they were public) through a "show
|
||
closed" filter in the RFC's tree view — closing is a state, not
|
||
a censorship event. Deleted branches are gone from Gitea but a
|
||
metadata row remains in the cache for historical reference.
|
||
|
||
The 30/90 timers reset on any push to the branch. Owners and
|
||
arbiters can pin a branch to disable the auto-close timer if the
|
||
work is paused but legitimately ongoing.
|
||
|
||
---
|
||
|
||
## 12. Branch hygiene policy (formalized)
|
||
|
||
| State | Trigger | Effect |
|
||
| ----------------------- | ----------------------------- | -------------------------------------------- |
|
||
| Open | branch created | normal contribution |
|
||
| Open, idle | 30 days no commit, no PR | auto-close |
|
||
| Closed | auto or manual close | hidden from default tree, surfaceable |
|
||
| Deleted | 60 days after close (90 from last activity) | branch removed from Gitea, row remains |
|
||
| Pinned | owner/arbiter pins | auto-close disabled |
|
||
|
||
Future story (not v1): out-of-band reopening of a deleted branch by
|
||
email request to an owner.
|
||
|
||
---
|
||
|
||
## 13. The graduation flow (super-draft → active RFC repo)
|
||
|
||
Graduation is initiated by an owner or admin clicking "Graduate to RFC
|
||
repo" on a super-draft's page. The button is disabled with a tooltip
|
||
when the super-draft has no owners (see §13.1) or when any meta-repo
|
||
body-edit PR is open against `rfcs/<slug>.md` (see §9.8 — open
|
||
body-edit PRs would attempt to re-introduce a body to a frontmatter-
|
||
only entry after step 3 of §13.3). Bare edit branches without an open
|
||
PR do not block graduation; they remain on the meta repo subject to
|
||
§12's hygiene timers.
|
||
|
||
### 13.1 Claim ownership (prerequisite)
|
||
|
||
If a super-draft has no owner, any signed-in contributor can click
|
||
"Claim ownership," which opens a PR against the meta repo adding their
|
||
username to the `owners:` field of the entry. Owners and admins can
|
||
merge. (A self-merge window for un-acted claims is not enabled in v1;
|
||
configurable later if needed.) Multiple claims simply append.
|
||
|
||
### 13.2 The Graduate dialog
|
||
|
||
Clicking "Graduate to RFC repo" opens a small dialog with three
|
||
editable fields:
|
||
|
||
- **Integer ID** — pre-filled as `max(existing integer IDs) + 1`,
|
||
formatted as `RFC-NNNN`. Editable to allow gap reservations but the
|
||
default is just the next number.
|
||
- **Repo name** — pre-filled as `rfc-NNNN-<slug>`, editable but
|
||
constrained to valid Gitea repo names.
|
||
- **Initial owners** — pre-filled from the entry's `owners:`, with an
|
||
"add owner" picker. Must have at least one.
|
||
|
||
Each field validates inline as the admin types, with a short
|
||
debounce, against the catalog cache and a regex — integer-ID
|
||
collision against existing IDs, repo-name pattern against valid
|
||
Gitea name rules, the at-least-one-owner constraint on the picker.
|
||
Errors render as a short line of text beneath the offending field.
|
||
The repo-name collision check is re-issued atomically server-side
|
||
on confirm, since a concurrent graduation could land between
|
||
dialog-open and submit. While any field is invalid, the confirm
|
||
button is disabled and its tooltip names the first blocker
|
||
specifically — "Integer ID 42 is already taken," "Repo name must be
|
||
lowercase letters, digits, and dashes," "Add at least one initial
|
||
owner" — the same grammar the precondition popover below uses, so
|
||
the dialog and the gate read as one surface rather than two
|
||
competing styles.
|
||
|
||
The dialog's confirm button is also disabled when the preconditions
|
||
from §13's opening paragraph fail — no owners on the entry, or any
|
||
open meta-repo PR against `rfcs/<slug>.md`. The disabled button
|
||
opens a small popover on hover or click that lists each failing
|
||
precondition as its own line item with an inline remediation
|
||
affordance per item. "No owners claimed yet" surfaces a "Copy share
|
||
link" affordance for surfacing the super-draft to a would-be
|
||
claimer, plus a secondary "Claim ownership yourself" — admins are
|
||
contributors per §6.1, so they can claim if they intend to graduate
|
||
solo. "N open body-edit PRs" expands inline within the popover to a
|
||
list of the offending PRs, one per row, carrying each PR's title,
|
||
author, and last-activity timestamp plus inline merge, withdraw,
|
||
and open-in-new-tab affordances; admins hold §6.3 authority on
|
||
those PRs and can resolve the precondition from the popover without
|
||
leaving the Graduate context.
|
||
|
||
The preconditions are enforced before the bot starts §13.3's
|
||
sequence, so §13.3's rollback complexity is unchanged.
|
||
|
||
### 13.3 The transactional sequence
|
||
|
||
Confirming the dialog runs this sequence as the bot:
|
||
|
||
1. Create the new Gitea repo.
|
||
2. Seed it with an initial commit on `main` containing:
|
||
- `README.md` (header pointing at the meta-repo entry, plus the
|
||
super-draft's pitch body migrated over).
|
||
- `RFC.md` (the actual document, starting from the super-draft body
|
||
or a template if the body is empty).
|
||
- `.rfc/metadata.yaml` — mirror of the meta-repo frontmatter for
|
||
future tooling.
|
||
3. Open a PR against the meta repo updating the entry: `state: active`,
|
||
`id: RFC-NNNN`, `repo: <new repo URL>`, `graduated_at: <timestamp>`,
|
||
`graduated_by: <admin username>`. The meta-repo entry's body field
|
||
is removed (frontmatter only, plus a generated "see the full RFC at
|
||
<repo>" link).
|
||
4. Auto-merge the PR (the same admin who clicked the button is the
|
||
merge actor).
|
||
5. Webhook flow updates the SQLite cache; left pane reflects the new
|
||
state immediately.
|
||
|
||
The dialog renders the sequence in flight as a stack of the five
|
||
named steps with per-step states — `pending`, `running`, `done`,
|
||
`failed`, `not reached` — and a one-line caption beneath the current
|
||
step naming the concrete operation ("Creating repository
|
||
wiggleverse/rfc-0042-open-human-model…"). The stack streams from
|
||
the server via the SSE surface in §17, one event per step
|
||
transition. On success, a brief "Graduation complete" frame holds
|
||
for a moment before the dialog closes and the catalog row
|
||
transitions per §7.2.
|
||
|
||
If any step fails partway, the app rolls back: deletes the
|
||
half-created repo, abandons the unmerged PR, surfaces a clear error
|
||
to the admin. The rollback is itself a visible step appended to the
|
||
stack on failure — the admin sees that cleanup ran, not just that
|
||
the act failed. The failed step turns red, later original-sequence
|
||
steps mark "not reached," and a "What happened" panel renders below
|
||
the stack explaining what was rolled back, what wasn't (if anything
|
||
is unrecoverable), and what to do next. The panel persists until the
|
||
admin dismisses it — a failure surface is not auto-dismissed.
|
||
Graduation is rare enough to afford this level of care.
|
||
|
||
### 13.4 Chat history follows the work
|
||
|
||
The chat thread attached to the super-draft moves to the new repo's
|
||
main-branch chat at graduation. This covers both the whole-doc main
|
||
thread per §8.4 and any range or paragraph sub-threads per §8.12
|
||
anchored to the super-draft's main view; anchors re-resolve against
|
||
`RFC.md` on the new repo and, where they fail, §8.12's stale
|
||
mechanic engages. The meta-repo entry retains a generated link
|
||
"Conversation continues at <repo URL>." The chat is about the RFC,
|
||
not the meta-repo entry, and it should travel with the work.
|
||
|
||
Meta-repo edit-branch chats, flag threads, and `changes` rows from
|
||
the super-draft phase **do not migrate**. They stay attached to
|
||
their original `branch_name` on the meta repo and surface from the
|
||
new RFC view via a **"Pre-graduation history"** affordance — a
|
||
straightforward lookup of `threads` and `changes` rows where
|
||
`rfc_slug = <slug>` and `branch_name` begins with `edit/<slug>/`
|
||
(the slug remains the canonical key per §2.3, before and after
|
||
graduation). UI affordance; no data movement, no rollback cost.
|
||
|
||
The affordance renders as a section in the §8.1 breadcrumb dropdown
|
||
on the new RFC view, alongside `main`, open branches, and open PRs,
|
||
headed "Pre-graduation history (N)" with each pre-graduation edit
|
||
branch listed as its own row. Selecting a row swaps the center
|
||
column to a read-only render of that branch's body at its last
|
||
commit and the right column to that branch's chat, with associated
|
||
change-cards and flags inline — the same machinery a closed branch
|
||
on an active RFC uses per §10.7 and §11.5. Anchors on pre-graduation
|
||
threads resolve against the pre-graduation body, not against
|
||
`RFC.md` on the new repo. The pre-graduation set is kept distinct
|
||
from the post-graduation "Show closed branches" filter in the same
|
||
dropdown — "branches that closed normally on this repo" and
|
||
"branches that lived on the meta repo before this repo existed" are
|
||
semantically different sets, and conflating them would obscure the
|
||
graduation hop.
|
||
|
||
### 13.5 Graduation is not reversible
|
||
|
||
Once an entry is graduated to `active`, the path forward is
|
||
`withdrawn`, not back to `super-draft`. Reversing graduation cleanly
|
||
is operationally messy (existing commits in the new repo, etc.) and
|
||
the cost of not having it is low — withdraw and re-graduate as a
|
||
fresh idea if needed.
|
||
|
||
---
|
||
|
||
## 14. Outside the RFC view: landing and the philosophy surface
|
||
|
||
The bulk of this spec describes what is inside an RFC and what is in
|
||
the left pane around it. This short section captures the structural
|
||
decisions about everything *outside* the RFC view — the app's chrome
|
||
and its public face.
|
||
|
||
### 14.1 Pre-login landing
|
||
|
||
The app's root URL, accessed by an unauthenticated visitor, renders a
|
||
landing page consisting of the title, the subtitle, and the short-form
|
||
deck from the top of `PHILOSOPHY.md` (see §2). Beneath the deck, a
|
||
single primary action: "Sign in with Gitea." Beneath that, a secondary
|
||
link: "Read the full philosophy" → `/philosophy`.
|
||
|
||
This is the front door. It sets expectation before the user encounters
|
||
the mechanics, so the mechanics (super-drafts, graduation, public
|
||
arguments, AI participation in chat) read as load-bearing rather than
|
||
novel.
|
||
|
||
### 14.2 The `/philosophy` route
|
||
|
||
Authenticated and anonymous visitors alike can reach `/philosophy`,
|
||
which renders the full body of `PHILOSOPHY.md` from the meta repo. The
|
||
content is sourced from the meta repo's main branch, cached and
|
||
refreshed on the same cadence as RFC bodies (§4). The page is plain
|
||
markdown rendering with no editing affordance.
|
||
|
||
### 14.3 Persistent "About" link
|
||
|
||
The app's header carries an unobtrusive link — "About" or "Why this
|
||
exists" — to `/philosophy`, available from every authenticated screen.
|
||
The philosophy is a reference document for the framework's mechanics,
|
||
not just marketing; a contributor mid-PR who wonders why a conversation
|
||
is public should be able to find the answer in two clicks.
|
||
|
||
### 14.4 What is not done
|
||
|
||
The philosophy is not pushed at returning users via banners or modals,
|
||
is not gated in front of the working surfaces, and is not embedded
|
||
inside the document pane. It earns its place by being available, not
|
||
by being insisted on.
|
||
|
||
The visual design of the landing page and the `/philosophy` route —
|
||
typography, layout, illustrations if any — is deferred. The structural
|
||
decisions above are the binding part.
|
||
|
||
---
|
||
|
||
## 15. Notifications
|
||
|
||
The framework's public-async-work model has accumulated signals across
|
||
every flow — proposal-merged, proposal-declined, contribute-granted,
|
||
PR-opened-on-watched-RFC, commit-added, review-thread-new, change-
|
||
proposed-on-edited-passage, graduation-ready, graduation-complete,
|
||
withdrawal — without committing a surface to receive them. This
|
||
section is that surface.
|
||
|
||
The framing constraint is from `PHILOSOPHY.md`: the framework is a
|
||
tool for thought, not a feed. Notifications exist because the public-
|
||
async work model breaks without them — a contributor whose thread of
|
||
work moves on Tuesday cannot first find out the following Saturday
|
||
when they happen to log in — and they must read as honest signal
|
||
rather than engagement bait. Defaults are conservative; the
|
||
contributor raises volume deliberately rather than dialing it down
|
||
under pressure. The system never invents attribution where none
|
||
exists (§15.9), never aggregates the user's own gestures into other
|
||
people's inboxes, and never optimizes for return visits as an end
|
||
in itself.
|
||
|
||
The signals themselves are produced by gestures specified elsewhere
|
||
in the spec. This section commits the **surface** that receives them,
|
||
the **subscription model** that decides whether a given user receives
|
||
a given signal, the **storage shape** that makes triage tractable, and
|
||
the **out-of-session channels** (email, digest) that let asynchrony
|
||
actually work.
|
||
|
||
### 15.1 The signal-surface stack
|
||
|
||
Five surfaces, each with one narrow job:
|
||
|
||
- **In-app inbox** — the durable, global triage surface. One mental
|
||
space across all RFCs the contributor has any relationship to, with
|
||
per-RFC and per-category filtering. The substrate for §15.2.
|
||
- **Badges** — ambient pull-ins. A single integer beside the inbox
|
||
icon in the app header (count of unread notifications); a small
|
||
binary dot on individual catalog rows for watched RFCs with unseen
|
||
activity per §7.2. No per-row count, no per-section count.
|
||
- **Toasts** — transient, in-session signals. Used only for the
|
||
contributor's own actions completing (proposal opened, change
|
||
accepted, graduation complete) and for arrivals during the current
|
||
session of signals about work the user is *actively viewing*.
|
||
Never the channel for activity elsewhere; that is what the inbox
|
||
is for.
|
||
- **Email** — out-of-session reach. Opt-in per category per §15.4,
|
||
conservative defaults. The single channel that escapes the app.
|
||
- **Digest** — aggregation. The catch-up surface for activity on
|
||
watched RFCs the user hasn't triaged through any of the real-time
|
||
channels. Cadence and dedup contract in §15.5.
|
||
|
||
The five compose, they do not overlap by accident. Each event is
|
||
routed to exactly the surfaces appropriate to its category, the
|
||
recipient's watch state, and the recipient's email/digest
|
||
preferences; the deduplication contract in §15.5 prevents the same
|
||
event from reaching the user twice through real-time and digest
|
||
paths.
|
||
|
||
**Event kinds.** The `notifications.event_kind` enum captures the
|
||
signal taxonomy this section commits to. The starting set:
|
||
`proposal_merged`, `proposal_declined`, `proposal_opened_on_watched_topic`,
|
||
`pr_opened`, `pr_merged`, `pr_withdrawn`, `pr_commit_added`,
|
||
`pr_review_thread_new`, `pr_review_thread_reply`,
|
||
`pr_conflict_with_main`, `chat_message_in_participated_thread`,
|
||
`chat_reply_to_my_message`, `change_proposed_on_edited_passage`,
|
||
`flag_dropped_on_watched_rfc`, `flag_resolved_on_my_flag`,
|
||
`contribute_grant_added`, `contribute_grant_revoked`,
|
||
`branch_pinned`, `branch_auto_closed`, `super_draft_graduation_ready`,
|
||
`graduation_complete`, `graduation_rolled_back`, `rfc_withdrawn`,
|
||
`rfc_reopened`, `claim_opened`, `claim_merged`,
|
||
`permission_change_affecting_me`, `app_wide_mute_set`,
|
||
`app_wide_mute_lifted`, `digest_emitted`. The enum is extensible; the
|
||
build session adjusts as new gestures are wired in.
|
||
|
||
### 15.2 The inbox
|
||
|
||
The inbox is a global view, reachable from a header icon present on
|
||
every authenticated screen alongside the §14.3 About link. Selecting
|
||
it opens a center-pane overlay (or a route, `/inbox`, depending on
|
||
the build session's chrome choice — the binding part is *one*
|
||
inbox, not per-RFC inboxes).
|
||
|
||
Each row carries: the actor's display name (or "system" attribution
|
||
for null-actor rows per §15.9), a verb phrase rendered from the event
|
||
kind plus payload, a scope chip (RFC title, branch name where
|
||
applicable, PR number where applicable), and a relative timestamp.
|
||
Clicking the row navigates to the work the signal is about and
|
||
marks the row read on next inbox refresh via the visit-advances-
|
||
cursor reconciler in §15.7. A `Mark read` affordance on each row is
|
||
available for triage without navigation.
|
||
|
||
Above the list, three controls: a **filter chip strip** (multi-
|
||
select, AND-combined: `Unread only`, `RFC: …`, `Category: personal-
|
||
direct / structural / churn`, `From: <user>`), a **bundle toggle**
|
||
(switches between per-row and per-RFC + per-event-kind grouping —
|
||
"3 new commits on PR #4 / RFC-0042" as a single bundle row,
|
||
markable in one gesture), and a **Mark all read** action that
|
||
respects the current filter (so the user can mark all churn read
|
||
without touching personal-direct rows).
|
||
|
||
The inbox is *not* the place watch-state preferences, email
|
||
categories, digest cadence, quiet hours, or per-user mutes are
|
||
configured — those live on a separate notification-settings surface
|
||
whose UX is the natural next topic (§19.2). The inbox is the triage
|
||
surface; the settings panel is where the underlying preferences
|
||
live.
|
||
|
||
### 15.3 Badges, toasts, and ambient signals
|
||
|
||
The badge in the app header reads the unread notification count
|
||
directly; it caps at 99+ visually and reads "99+" rather than
|
||
escalating into a four-digit number that contributes nothing to
|
||
triage. Tapping the badge opens the inbox; the count refreshes from
|
||
the same SSE stream that backs the inbox itself per §17, so badge
|
||
and inbox never disagree.
|
||
|
||
The per-row catalog dot from §7.2 is binary, not numeric. Its rule:
|
||
visible on a row when the row's RFC has any unread notification for
|
||
the signed-in user *and* the user's watch state on that RFC is
|
||
`watching` or `following` (per §15.6). The dot clears the moment
|
||
the unread count for that RFC reaches zero, whether through inbox
|
||
mark-read or through in-context visit-advances-cursor reconciliation
|
||
per §15.7.
|
||
|
||
Toasts surface in three narrow cases. First, the user's own action
|
||
completing — proposal opened, change accepted/declined, PR opened,
|
||
graduation confirmed; these are mid-session feedback for a gesture
|
||
the user just made. Second, an event firing on the *exact view the
|
||
user is currently looking at* — a commit arriving on a PR the user
|
||
is viewing, a chat reply landing in a branch chat the user has open;
|
||
these are scoped to the live view and never persist (the inbox row
|
||
still appears for the same event, but the toast carries the
|
||
"something just happened here" beat). Third, the system-author
|
||
messages per §10.6 and §8.6 are *not* toasts — they are inline chat
|
||
content; toasts are a chrome surface, not a content surface. Toasts
|
||
are dismissible by gesture and auto-dismiss after a short interval
|
||
(implementation-detail timing); the chrome never stacks more than a
|
||
small number of toasts on top of each other (a fourth arriving while
|
||
three are visible queues, not stacks).
|
||
|
||
The contribute-mode entry in §8.3, the graduation-complete frame in
|
||
§13.3, and the proposal-merged/declined banners in §9.3 already
|
||
carry their own visual beats and are not duplicated as toasts.
|
||
|
||
### 15.4 Email
|
||
|
||
Email is the single channel that escapes the app. The discipline
|
||
governing it: opt-in per category with category-specific defaults,
|
||
body mirrors the inbox row text verbatim, one-click unsubscribe per
|
||
category, single non-spoofing From identity.
|
||
|
||
Four categories, each with a distinct default and rationale:
|
||
|
||
- **Personal-direct events — default on.** Signals where the
|
||
recipient is the named subject: proposal merged or declined (§9.3,
|
||
recipient is the proposer); graduation completed on a super-draft
|
||
the recipient owns; contribute grant added or revoked (recipient
|
||
is the grantee); reply directly to a thread the recipient posted
|
||
in; AI proposal targeted at a passage the recipient previously
|
||
edited (§8.11); the recipient's PR merged or withdrawn by someone
|
||
else. The contract: when the contributor's name is on the action,
|
||
the system reaches out of band. Opt-in here would let the contract
|
||
fail silently.
|
||
- **Watched-RFC structural events — default off.** PR opened on a
|
||
watched RFC, PR merged, graduation, withdrawal, decline-of-
|
||
someone-else's-proposal — the structural beats on RFCs the
|
||
recipient watches or follows but is not personally subject to.
|
||
Inbox and badges already carry these; the email toggle is opt-in
|
||
for contributors who want out-of-session reach.
|
||
- **Watched-RFC churn — never email.** New commits on a PR the
|
||
recipient did not open, additional chat messages in a thread the
|
||
recipient did not participate in, new flags on a watched RFC. The
|
||
email toggle for this category appears in settings as permanently
|
||
disabled, with a tooltip naming the refusal explicitly: *"Per-
|
||
commit and per-message email is intentionally not offered. The
|
||
digest aggregates this activity weekly."* Naming the refusal is
|
||
more honest than silently omitting the toggle.
|
||
- **Admin-actionable events — default on for admins/owners,
|
||
unused for contributors.** Super-draft graduation-ready (an
|
||
owner has been claimed and no body-edit PRs are blocking per
|
||
§13.2); mute/restore events affecting the recipient as the actor
|
||
or subject; permission-change events affecting the recipient.
|
||
Admin actions are time-pressured against waiting contributors.
|
||
The column on `users` is unused for contributors and ignored at
|
||
generation time.
|
||
|
||
The email envelope: subject `[Wiggleverse] <Event>: <RFC title or
|
||
super-draft title>`. From: a single noreply identity (e.g.
|
||
`Wiggleverse <notifications@…>`), regardless of which user's gesture
|
||
produced the signal per §15.9. Body: the inbox row's rendered text
|
||
verbatim, plus a one-line context line ("by @alice on
|
||
RFC-0042 / branch <name> · 5 minutes ago"), plus one primary link to
|
||
the changed thing. No marketing footer. Two trailing links:
|
||
`Unsubscribe from <this category>` (one-click, no confirmation page;
|
||
signed URL, idempotent) and `Manage all preferences`.
|
||
|
||
Email is held during the recipient's quiet hours per §15.8 and
|
||
released at window end; held messages bundle into a single "Activity
|
||
while you were away" email when more than a small threshold (the
|
||
build session can set, in the neighborhood of five) accumulated,
|
||
otherwise sending individually at window end.
|
||
|
||
Bounces and complaints route to the global email opt-out path
|
||
automatically. The `users` table does not need a separate bounce
|
||
column — a global opt-out is the only durable response to a hard
|
||
bounce.
|
||
|
||
### 15.5 The digest
|
||
|
||
The digest is the catch-up surface for activity on watched RFCs
|
||
that the recipient hasn't triaged through any other channel. It
|
||
runs on its own cadence and carries the *churn* class that §15.4's
|
||
email policy intentionally excluded.
|
||
|
||
Default cadence is **weekly**, fired Sunday evening UTC (most
|
||
regions wake up to it Monday morning). Per-user configurable on the
|
||
`users.digest_cadence` column to `daily`, `weekly`, or `off`. The
|
||
digest is not sent if there is nothing to report.
|
||
|
||
Coverage is **event-window**, not time-window: each digest covers
|
||
"everything since the recipient's last digest," tracked via the
|
||
`notification_digests` row's `period_start` / `period_end`. A
|
||
recipient switching cadence — weekly to daily, or off back to
|
||
weekly — does not see overlapping coverage.
|
||
|
||
Three exclusion rules, applied in order at digest assembly time:
|
||
|
||
1. Exclude any notification with `email_sent_at` set — those were
|
||
already triaged out-of-session via the personal-direct or
|
||
structural email categories. The digest does not re-report things
|
||
the recipient already saw in email.
|
||
2. Exclude any notification with `read_at` set — those were
|
||
triaged in the inbox or in-context via the visit-advances-
|
||
cursor reconciler in §15.7.
|
||
3. Include all remaining notifications, but annotate any still-
|
||
unread row as "still unread in your inbox." The digest is the
|
||
catch-up surface; suppressing unread items would defeat its
|
||
purpose, but the recipient deserves the disclosure that the
|
||
item is double-tracked.
|
||
|
||
Each included notification's `digest_included_at` is set when the
|
||
digest is emitted, making the three rules idempotent and queryable
|
||
at audit time.
|
||
|
||
Format: subject `[Wiggleverse] Weekly digest — N events across M
|
||
RFCs` (or `Daily digest`). Body in spec voice: a single summary
|
||
sentence, then per-RFC sections ordered by activity volume, each
|
||
section grouping events by kind — "3 PRs opened on RFC-0042 (Open
|
||
Human Model): #12, #13, #14"; "5 commits on PR #4 (still open)";
|
||
"12 new chat messages across 3 threads"; "2 flags dropped." Each
|
||
event line links to the work; the per-RFC section header links to
|
||
the RFC view. Trailing links match §15.4's email envelope:
|
||
`Manage digest preferences`, `Manage all preferences`.
|
||
|
||
The digest does not aggregate personal-direct events (those have
|
||
their own email channel), does not include events on RFCs the user
|
||
has muted via the `watches.muted` state, and does not send to
|
||
admins for admin-actionable events (those are personal-direct).
|
||
|
||
### 15.6 The watch / subscription model
|
||
|
||
Three watch levels per RFC, all stored on the `watches` table per
|
||
§5: `watching`, `following`, `muted`. Each row is per (user, RFC);
|
||
absence of a row means no relationship and no signal generation.
|
||
|
||
Watches are **implicit, earned by gestures**, with manual override
|
||
always available. The auto-rules:
|
||
|
||
- A **substantive gesture** sets `watching` if no row exists, or
|
||
upgrades `following` → `watching` (never downgrades). The
|
||
substantive gestures: post in a chat thread; create a branch;
|
||
open a PR; accept or decline a change; claim ownership; receive
|
||
a contribute grant; drop a flag; merge a PR; resolve a thread.
|
||
- **Starring** sets `following` if no row exists. Star never
|
||
downgrades a `watching` row to `following` (the substantive
|
||
gesture wins); star never overrides an explicit `muted` (the
|
||
explicit setting wins). The star itself is independent of the
|
||
watch row — unstarring does not change the watch state.
|
||
- **Role assignment** as arbiter or owner per §6.3 implicitly
|
||
treats the RFC as `watching` for the purpose of signal
|
||
generation, regardless of `watches` row state. The role *is*
|
||
the subscription, and it never auto-decays. An owner or
|
||
arbiter can still explicitly set `muted` on a row; the explicit
|
||
mute wins over the role-implicit watch, which is intentional
|
||
(an admin should be able to step back from a specific RFC's
|
||
notifications even while keeping the authority).
|
||
|
||
Each level's signal scope:
|
||
|
||
- **`watching`** — the full stream for the RFC: PR open, PR merged,
|
||
PR commits, PR review-thread activity, chat messages in
|
||
participated-in threads, change-proposed-on-edited-passage, flag
|
||
drops, flag resolutions on the recipient's own flags, contribute
|
||
grants on branches the recipient has touched, structural events
|
||
(graduation, withdrawal, reopen). Plus all personal-direct
|
||
events that happen to land on this RFC.
|
||
- **`following`** — structural beats only: PR open, PR merged,
|
||
graduation, withdrawal, decline-of-others'-proposals. No commit-
|
||
level churn, no chat noise, no flag drops on threads the
|
||
recipient didn't participate in. Plus all personal-direct
|
||
events that happen to land on this RFC.
|
||
- **`muted`** — no signals from this RFC, including no personal-
|
||
direct events on this RFC. The muted state is explicit-only and
|
||
is the strongest "leave me alone about this" gesture available.
|
||
|
||
Per-PR and per-thread signaling is **derived from existing
|
||
participation tables**, not stored on a separate `pr_watches` or
|
||
`thread_watches` table:
|
||
|
||
- A user receives signals about a PR if they have ever advanced
|
||
`pr_seen` on it, have a row in `changes.acted_by` on its branch,
|
||
or are the PR's opener. These are the "you touched this PR"
|
||
signals — the muted state on the RFC suppresses them.
|
||
- A user receives signals about a thread if `thread_messages`
|
||
carries a row authored by them in that thread. The muted state
|
||
on the RFC suppresses them.
|
||
|
||
This keeps the watch surface narrow (one row per user × RFC) while
|
||
preserving the precision the inbox needs.
|
||
|
||
**90-day decay.** A `watching` row that has not accumulated a
|
||
substantive gesture from the user in 90 days auto-decays to
|
||
`following`. The `watches.last_participation_at` column tracks the
|
||
last-substantive-gesture timestamp; a nightly job downgrades rows
|
||
whose timestamp is older than the threshold. Star never decays;
|
||
explicit settings never decay; role-implicit watching never decays
|
||
(it is not stored on the row, it is computed). On the user's next
|
||
visit to the decayed RFC, a small unobtrusive line in the chrome
|
||
notes the change ("Following since [date]"); a one-click affordance
|
||
restores `watching`.
|
||
|
||
A small **watch-state affordance** on the RFC view header lets the
|
||
user move between the three levels at any time. Explicit choices
|
||
set `set_by = 'explicit'` and are exempt from auto-decay. Manual
|
||
override always wins over auto-rules.
|
||
|
||
### 15.7 The unread mechanism
|
||
|
||
Two cursor families serve two different jobs, and the section is
|
||
careful not to conflate them.
|
||
|
||
**The notifications cursor — per-event read state.** Each row in
|
||
`notifications` carries its own `read_at`. The inbox renders unread
|
||
rows accented and clears the accent on read. The unread count
|
||
backing the header badge is `COUNT(*) WHERE recipient = me AND
|
||
read_at IS NULL`. Marking a row read is per-row, per-bundle, or
|
||
per-filter (the `Mark all read` action). Per-event read state is
|
||
what the inbox needs because triage is per-event: "I read the email
|
||
about the decline; the corresponding inbox row should be marked
|
||
read; but I haven't visited the proposal page yet."
|
||
|
||
**The scope cursors — within-scope freshness.** `pr_seen` (§10.3)
|
||
and `branch_chat_seen` (§5) both track per-user, per-scope cursors
|
||
for the in-context "what changed since my last visit" accent
|
||
inside a PR review surface or a branch chat. These are *within-
|
||
scope* cursors; they answer "have I seen the contents of *this
|
||
scope*?" rather than "have I triaged the signal that *this scope*
|
||
moved?"
|
||
|
||
**Reconciliation between the two.** Visiting the work advances the
|
||
scope cursor (per §10.3 for PRs, per §5 for branch chats); a
|
||
post-visit reconciler then marks related notifications read on
|
||
next inbox refresh. The reconciler's rule: for each scope cursor
|
||
that just advanced, find all `notifications` rows where
|
||
`recipient_user_id = me` AND `(rfc_slug, pr_number)` or
|
||
`(rfc_slug, branch_name)` matches the advanced scope AND `read_at
|
||
IS NULL` AND the event's logical timestamp is at or before the new
|
||
cursor position. Set `read_at` for those rows. This closes the
|
||
loop in the visit-advances-triage direction; the inverse (marking
|
||
the inbox row read does not advance the scope cursor) is also
|
||
intentional, because the user may know enough from the email or
|
||
inbox text not to need to visit the work yet, and we do not want
|
||
the scope cursor falsely claiming the user has read the diff.
|
||
|
||
Per-signal vs per-bundle read advance: the inbox renders each
|
||
notification as its own row by default; the bundle toggle from §15.2
|
||
collapses rows by RFC + event kind, and marking a bundle read marks
|
||
all its constituent rows. Both modes operate on the same per-row
|
||
`read_at` column.
|
||
|
||
### 15.8 Do-not-disturb
|
||
|
||
Three mechanisms, each narrowly scoped:
|
||
|
||
- **Quiet hours.** A single per-user window in the user's local
|
||
time, stored in `users.notification_quiet_hours_start /
|
||
_end / _timezone`. Default off (all columns null). During the
|
||
window, email and digest delivery is held; inbox and badges are
|
||
unaffected (they do not make sound). At window end, held
|
||
messages bundle into a single "Activity while you were away"
|
||
email per §15.4 if more than the bundling threshold accumulated;
|
||
otherwise they fire individually. A single window is the
|
||
schema commitment — workday quiet hours separately from
|
||
nighttime quiet hours is the kind of refinement that earns its
|
||
schema later, after evidence.
|
||
- **Per-RFC mute.** The `watches.state = 'muted'` setting from
|
||
§15.6. No separate mechanism. Suppresses all signal generation
|
||
for the (user, RFC) pair, including personal-direct events
|
||
scoped to that RFC.
|
||
- **Per-user mute.** A row in `notification_user_mutes` keyed by
|
||
(muter_user_id, muted_user_id) suppresses any notification whose
|
||
`actor_user_id` matches the muted user. Notification-volume
|
||
only; does not affect content visibility in any view — the muted
|
||
user's posts, commits, flags, and review threads remain
|
||
readable. The framework's evidence claim depends on the muted
|
||
user's words remaining visible; muting their *name* in the
|
||
recipient's inbox is the narrow tool the recipient needs.
|
||
|
||
Per-user mute is **not available** to admins or owners exercising
|
||
app-wide authority, and not available to arbiters exercising
|
||
§6.3 authority on RFCs where they hold it. The role
|
||
contractually requires receiving signals from everyone; muting
|
||
one's way out of an arbitration responsibility undercuts the
|
||
role. The UI surfaces the constraint at the moment of attempted
|
||
mute, with the rationale inline rather than as a silent disable.
|
||
|
||
These three are strictly orthogonal to §6.2's app-wide write-mute,
|
||
per the clarification folded into §6.2 in this section's pass.
|
||
|
||
### 15.9 Notification authorship
|
||
|
||
Notifications surface the **underlying user** as actor, not the
|
||
bot. The `notifications.actor_user_id` column holds the underlying
|
||
user's id for any event produced by a human gesture; the inbox row
|
||
reads "@alice opened PR #4 on RFC-0042 (Open Human Model)," and
|
||
the email subject reads "[Wiggleverse] @alice opened PR #4:
|
||
<title>." Triage requires the human's name in the noun slot — the
|
||
reader's first question is "whose work is this?" and surfacing the
|
||
bot puts the wrong noun in the way.
|
||
|
||
**System-generated events have `actor_user_id = null`.** The auto-
|
||
close at 30 days (§11.5, §12), the digest emission, the anchor-
|
||
lost transition on threads (§8.12), the graduation-readiness
|
||
detection (§13.1), the post-graduation catalog transition (§7.2)
|
||
— none have a human actor. The inbox row names no one: "PR #4
|
||
auto-closed (30 days idle)." Absence of an actor is the honest
|
||
signal that no human acted; the system does not invent attribution.
|
||
|
||
**The bot account does not appear in notifications.** It remains
|
||
the §1 Git-layer persona that opens commits and PRs on behalf of
|
||
users, and §6.5's `On-behalf-of:` trailer remains the Git-side
|
||
accountability mechanism the audit log relies on. Surfacing the
|
||
bot on the notification side would conflate two distinct
|
||
accountability boundaries — the Git-layer "who opened this PR"
|
||
and the social "whose argument is this" — and the conflation
|
||
would cost the framework the precision it depends on. The same
|
||
underlying-actor-not-bot rule applies to **every other user-visible
|
||
attribution surface** — the §7.3 pending-ideas disclosure's
|
||
"by @alice," the §10.3 PR header's opener line, any in-app
|
||
rendering of "who did this." The §4 cache resolves the actor for
|
||
these surfaces by joining against the §6.5 `actions` log keyed on
|
||
`(rfc_slug, pr_number, action_kind)`, falling back to parsing the
|
||
`On-behalf-of:` trailer when the audit log row is absent (e.g.
|
||
when the cache was rebuilt from a meta repo populated outside the
|
||
app's history). The bot login surfaces only in the Git log and in
|
||
the `On-behalf-of:` trailer itself, never as a noun in the app's
|
||
prose.
|
||
|
||
**Email envelope identity is a single non-spoofing address** —
|
||
`Wiggleverse <notifications@…>` — regardless of which user's
|
||
gesture produced the signal. Spoofing the actor's email address
|
||
creates SPF/DKIM trouble for legitimate domains and gets the
|
||
framework's mail marked spam; surfacing two competing identities
|
||
(bot in From, user in subject) breaks email-client filtering rules
|
||
that key on From. One stable envelope identity, the actor named in
|
||
content.
|
||
|
||
---
|
||
|
||
## 16. What is deliberately deferred
|
||
|
||
Calling these out so the follow-up session knows what is *not yet
|
||
specified* and what is intentionally out of scope for v1.
|
||
|
||
- **The main document pane.** How `RFC.md` (or the super-draft body)
|
||
renders, the Tiptap editor configuration, how the editor handles
|
||
rich markdown, headings, links, code blocks.
|
||
- **The per-RFC and per-branch chat UX.** Threading model, AI
|
||
participation, the discuss-vs-contribute mode distinction from the
|
||
prototype, the selection tooltip, the prompt bar, model picker.
|
||
- **The revision flow.** How proposed changes from AI or contributors
|
||
appear, the change-card panel, accept/decline/edit, tracked
|
||
insertions/deletions in the editor.
|
||
- **Body full-text search.** Title/slug/ID/tag search ships in v1;
|
||
body content search is a later layer.
|
||
- **Featured super-drafts.** A `featured: true` frontmatter flag for
|
||
owner/admin curation. One-line addition we can ship when the need
|
||
is real.
|
||
- **Accepted / deprecated states.** Reintroducible if an OHM moment
|
||
("this is now official") becomes desirable.
|
||
- **Raw `git clone` + push as a contribution path.** All contribution
|
||
flows through the app.
|
||
- **Self-merge window for un-acted ownership claims.** Configurable;
|
||
off in v1.
|
||
|
||
---
|
||
|
||
## 17. Backend surfaces (API shape, illustrative)
|
||
|
||
The follow-up session will refine this. A minimal starting set:
|
||
|
||
- `GET /api/rfcs` — list entries with state, id, title, slug, repo,
|
||
owners, last_active_at, has_open_prs, starred-by-me. Supports
|
||
search, sort, filter chips, and the `unclaimed` predicate.
|
||
- `GET /api/rfcs/<slug>` — one entry's full metadata plus tree
|
||
view (open PRs, open branches, closed branches if requested).
|
||
- `GET /api/rfcs/<slug>/main` — main-branch body.
|
||
- `GET /api/rfcs/<slug>/branches/<branch>` — branch body and metadata.
|
||
- `POST /api/rfcs/propose` — open the idea-submission PR per §9.1.
|
||
Body carries `title`, `slug`, `pitch`, `tags`. Server-validates
|
||
slug uniqueness against `rfcs/` on the meta-repo main and against
|
||
the slugs of any open idea PRs. Returns the new PR number plus a
|
||
redirect handle for the pending-idea view (§9.3).
|
||
- `GET /api/proposals` — list open idea PRs for the pending-ideas
|
||
disclosure in §7.3.
|
||
- `GET /api/proposals/<pr_number>` — pending-idea view data per
|
||
§9.3: the proposed file's body and frontmatter, the PR title and
|
||
description, the chat thread id, the viewer's available
|
||
affordances.
|
||
- `POST /api/proposals/<pr_number>/withdraw` — proposer-only;
|
||
closes the meta-repo PR per §9.3.
|
||
- `POST /api/proposals/<pr_number>/decline` — owner/admin only;
|
||
closes the meta-repo PR with a required comment per §9.3.
|
||
- `POST /api/proposals/<pr_number>/merge` — owner/admin only;
|
||
merges the meta-repo PR, creating the super-draft entry on main.
|
||
Threads on the pending idea become the super-draft's main-chat
|
||
threads per §9.3 with no data movement.
|
||
- `POST /api/rfcs/<slug>/claim` — open the ownership-claim PR per
|
||
§13.1.
|
||
- `POST /api/rfcs/<slug>/metadata` — title or tag edits on a
|
||
super-draft per §9.5's metadata pane; opens a small meta-repo PR
|
||
via the bot. Slug renames not supported in v1.
|
||
- `POST /api/rfcs/<slug>/start-edit-branch` — the "Start
|
||
Contributing" gesture on a super-draft per §9.5; cuts a fresh
|
||
meta-repo branch `edit/<slug>/<auto-name>`, returns the branch's
|
||
metadata, lands the contributor in contribute mode. The body
|
||
optionally carries a desired branch name; otherwise auto-generated.
|
||
- `POST /api/rfcs/<slug>/graduate` — run the graduation sequence
|
||
(admin/owner only). Body carries the dialog's three fields —
|
||
candidate integer ID, candidate repo name, initial owners. Returns
|
||
a stream handle for the progress SSE below; the body fields are
|
||
re-validated atomically server-side at the top of the sequence
|
||
(the repo-name collision check in particular) since a concurrent
|
||
graduation could land between dialog-open and confirm.
|
||
- `GET /api/rfcs/<slug>/graduate/check` — inline validation for the
|
||
Graduate dialog per §13.2. Query params: `id` (the candidate
|
||
integer ID), `repo` (the candidate repo name). Returns per-field
|
||
collision/validity status from the catalog cache plus a
|
||
server-authoritative repo-name collision check. Debounced from
|
||
the client; the dialog calls this as the admin types.
|
||
- `GET /api/rfcs/<slug>/graduate/progress` — SSE stream of the
|
||
five-step transactional sequence per §13.3, one event per step
|
||
transition (`pending` → `running` → `done` / `failed`), plus a
|
||
trailing `rollback` step's events if any earlier step fails. The
|
||
Graduate dialog opens this stream on confirm and renders the step
|
||
stack from the events. The stream closes on success or on
|
||
rollback completion.
|
||
- `GET /api/rfcs/<slug>/blocking-prs` — list open meta-repo PRs
|
||
against `rfcs/<slug>.md` per §13.2's precondition popover. Returns
|
||
PR number, title, author, last-activity timestamp, and the
|
||
viewer's available actions (merge, withdraw, open-in-new-tab) per
|
||
§6.1 / §6.3.
|
||
- `POST /api/rfcs/<slug>/withdraw` — withdraw an entry.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/visibility` — set
|
||
read_public and contribute_mode.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/grants` — add/remove
|
||
per-branch contribute grants.
|
||
- `POST /api/rfcs/<slug>/branches/main/promote-to-branch` — the
|
||
`Start Contributing` flow on main per §8.14: cuts a new branch
|
||
from main's tip, re-anchors pending `changes` rows from `main` to
|
||
the new branch's name, returns the new branch's metadata. Body
|
||
optionally carries a desired branch name; otherwise an
|
||
auto-generated value is used.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/changes/<change_id>/accept`
|
||
— accept a pending change per §8.9; runs the immediate commit per
|
||
§8.6. Body carries the possibly-edited `proposed` text, the
|
||
`was_edited_before_accept` flag, and an optional
|
||
`force_apply_stale` flag for the stale-change confirmation path
|
||
per §8.11.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/changes/<change_id>/decline`
|
||
— decline per §8.9. No commit; row state moves to `declined` and
|
||
persists as evidence.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/changes/<change_id>/reask`
|
||
— regenerate a stale AI proposal against current text per §8.11.
|
||
Triggers a fresh streaming assistant message in the originating
|
||
thread; the old row stays for audit.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/manual-flush` — explicit
|
||
save gesture for buffered manual edits per §8.11; the API
|
||
equivalent of the in-card `Save now` button. Idempotent for an
|
||
empty buffer.
|
||
- `GET /api/rfcs/<slug>/branches/<branch>/threads` — list threads on
|
||
the branch with filtering by `state` and `thread_kind`. Includes
|
||
the whole-doc default thread.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/threads` — create a thread
|
||
per §8.12 or §8.13. Body: `thread_kind`, `anchor_kind`,
|
||
`anchor_payload`, `label` (required for `flag`, optional for
|
||
`chat`), and an optional first `message` for chat-kind.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/threads/<thread_id>/messages`
|
||
— post a message into a thread per §8.12; the same endpoint is the
|
||
reply path. Body: `text`, optional `quote`.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/threads/<thread_id>/resolve`
|
||
— resolve a thread per §8.12; permission per the rules in that
|
||
section.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/open-pr` — open a PR per
|
||
§10.1; body carries the AI-drafted (and possibly edited) title and
|
||
description.
|
||
- `GET /api/rfcs/<slug>/prs/<pr_number>` — PR metadata, diff handle,
|
||
thread ids for the conversation and review surfaces.
|
||
- `POST /api/rfcs/<slug>/prs/<pr_number>/seen` — advance the per-user
|
||
seen-cursor (§10.3).
|
||
- `POST /api/rfcs/<slug>/prs/<pr_number>/review` — post a review-kind
|
||
thread anchored to a diff range per §10.4.
|
||
- `POST /api/rfcs/<slug>/prs/<pr_number>/merge` — merge per §10.5
|
||
(arbiter/admin/owner only).
|
||
- `POST /api/rfcs/<slug>/prs/<pr_number>/withdraw` — withdraw per §10.8.
|
||
- `POST /api/rfcs/<slug>/prs/<pr_number>/resolution-branch` — cut a
|
||
fresh resolution branch and replay per §10.9.
|
||
- `POST /api/admin/users/<id>/role` — set role (owner/admin only).
|
||
- `POST /api/admin/users/<id>/mute` — mute/unmute (the §6.2 app-wide
|
||
write-mute, not the §15.8 notification mutes).
|
||
- `POST /api/stars/<slug>` — star/unstar.
|
||
- `POST /api/webhooks/gitea` — webhook receiver.
|
||
- `GET /api/notifications` — list inbox rows for the signed-in user,
|
||
per §15.2. Query params: `unread` (bool), `rfc_slug`, `category`
|
||
(`personal-direct` | `structural` | `churn`), `actor_user_id`,
|
||
`bundled` (bool — server-side grouping by RFC + event_kind per
|
||
§15.2). Returns row payload plus the unread count for the badge.
|
||
- `POST /api/notifications/<id>/read` — mark a single row read.
|
||
- `POST /api/notifications/read` — mark read by filter; body carries
|
||
the same filter params as the list endpoint. Used by `Mark all
|
||
read` and by bundle-collapse `mark read` actions per §15.2.
|
||
- `GET /api/notifications/stream` — SSE stream of inbox additions
|
||
and read-state changes for the signed-in user. Backs the live
|
||
inbox refresh and the header badge counter per §15.3.
|
||
- `GET /api/watches` — list the signed-in user's watch rows per
|
||
§15.6, with per-row state and `set_by`.
|
||
- `POST /api/rfcs/<slug>/watch` — set watch state explicitly per
|
||
§15.6; body carries `state` (`watching` | `following` | `muted`).
|
||
Sets `set_by = 'explicit'` and exempts the row from the 90-day
|
||
auto-decay.
|
||
- `POST /api/rfcs/<slug>/branches/<branch>/chat-seen` — advance the
|
||
per-user `branch_chat_seen` cursor per §5 / §15.7; body carries
|
||
`last_seen_message_id`. Triggers the inbox reconciler for
|
||
chat-kind notifications scoped to this branch.
|
||
- `GET /api/users/me/notification-preferences` — read the per-user
|
||
email category toggles and digest cadence per §15.4 / §15.5.
|
||
- `POST /api/users/me/notification-preferences` — set them.
|
||
- `GET /api/users/me/quiet-hours` — read the per-user quiet-hours
|
||
window per §15.8.
|
||
- `POST /api/users/me/quiet-hours` — set or clear it; body carries
|
||
start, end, timezone (all required to set; all null to clear).
|
||
- `POST /api/users/<id>/notification-mute` — add a per-user
|
||
notification mute per §15.8. Idempotent. Refused if the signed-in
|
||
user is acting in an admin/owner authority capacity or as an
|
||
arbiter on an RFC where the muted user is also active.
|
||
- `DELETE /api/users/<id>/notification-mute` — remove it.
|
||
- `GET /api/email/unsubscribe` — one-click per-category
|
||
unsubscribe per §15.4. Signed URL; idempotent; redirects to a
|
||
short confirmation page.
|
||
- `POST /api/webhooks/email-bounce` — bounce and complaint receiver
|
||
per §15.4; sets the recipient's global email opt-out.
|
||
|
||
Plus all the chat / streaming / model-picker endpoints inherited from
|
||
the prototype, scoped to per-RFC and per-branch threads.
|
||
|
||
The `branches/<branch>/...` endpoint family covers both per-RFC-repo
|
||
branches and meta-repo edit branches; per §9.5, the routing collapses
|
||
on a single rule — when `<slug>` resolves to an entry in state
|
||
`super-draft`, `<branch>` names a branch on the meta repo. The
|
||
endpoint implementations dispatch on the entry's state to pick the
|
||
right Gitea repo; the API surface is the same.
|
||
|
||
---
|
||
|
||
## 18. Carryover from the prototype
|
||
|
||
These are confirmed unchanged from the existing app and should be
|
||
preserved as-is in the rewrite unless a downstream decision changes
|
||
them:
|
||
|
||
- FastAPI + SSE for streaming chat.
|
||
- React + Vite + Tiptap (ProseMirror) for the editor.
|
||
- The structured `<change>` / `<original>` / `<proposed>` / `<reason>`
|
||
protocol for AI-proposed edits.
|
||
- Multi-provider LLM support: Anthropic Claude, Google Gemini, OpenAI /
|
||
GitHub Copilot. `ENABLED_MODELS` and per-provider API keys via env.
|
||
- The discuss-vs-contribute distinction inside an RFC view (to be
|
||
fully specified in the follow-up session).
|
||
- Gitea OAuth for user authentication. The OAuth identity is the basis
|
||
for the app's user account; authorization is layered on top per §6.
|
||
|
||
---
|
||
|
||
## 19. Topics still to settle
|
||
|
||
This spec captured the topics settled across thirteen sessions —
|
||
the structural model (repos, states, storage, permissions, list),
|
||
the RFC view (document, chat, branches, changes, threads, flags),
|
||
the super-draft view and lifecycle, the PR flow, the graduation
|
||
flow, the chrome (landing, philosophy), and now the notification
|
||
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
|
||
|
||
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`.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
**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 next build session should read `SPEC.md`, `README.md`,
|
||
`docs/DEV.md`, and this §19.1 entry 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.
|
||
|
||
### 19.2 Candidate topics for sessions after the v1 build lands
|
||
|
||
These remain unsettled and will earn their own sessions as the
|
||
build surfaces evidence for which one matters next. Topics are
|
||
listed roughly in order of expected weight; the order is not
|
||
binding.
|
||
|
||
- **Per-RFC model availability and credential delegation.** Which
|
||
AI models contributors can pick from when chatting on a given
|
||
RFC, and who supplies the API resources for those models.
|
||
Replaces §18's app-level `ENABLED_MODELS` and env-supplied keys
|
||
with per-RFC-scoped configuration. Touches every AI surface —
|
||
every chat thread (§8.12), every change-proposal turn (§8.9,
|
||
§8.11), every flag-resolution invocation (§8.13), the AI-drafted
|
||
PR title and description (§10.2), and the propose modal's
|
||
AI-suggested tags (§9.1). Touches §5 (schema for model config
|
||
and credentials), §6 (possibly a `funder` role, or a per-RFC
|
||
capability extension along the lines of §6.2's per-user
|
||
overrides), and §18 (carryover supersession). Subdividable into
|
||
"model availability" (lighter, UX-shaped) and "credential
|
||
delegation and the funder role" (heavier — security, billing,
|
||
abuse mitigation, rotation, mid-conversation key failure) if the
|
||
session driver judges the combined scope too large. Load-bearing
|
||
once the framework runs past single-operator deployment;
|
||
defer-able until then.
|
||
- **Admin surfaces.** Where role management, muting, audit-log
|
||
views, the graduation-readiness queue, and Topic 13's
|
||
notification-preferences settings (email categories per §15.4,
|
||
digest cadence per §15.5, quiet hours per §15.8, per-user mute
|
||
list per §15.8, watch-state overview per §15.6) live in the
|
||
chrome. Topics 12 and 13 both expanded the admin's repertoire
|
||
without giving it a centralized home base; consolidating it is
|
||
the natural next move once the build is on its feet.
|
||
- **The notification settings UI.** Topic 13 settled the schema
|
||
and the per-category rules; the surface where a contributor
|
||
finds the per-category email toggles, digest cadence, quiet
|
||
hours config, watch states, and per-user mute list is the
|
||
natural follow-on. Likely overlaps the admin-surfaces topic for
|
||
admins and stands alone for contributors. Small-to-medium scope.
|
||
- **The conflict-replay UX in detail.** §10.9 commits to a
|
||
resolution-branch path where the AI participant replays the diff
|
||
onto fresh main, with manual fallback for ambiguous conflicts.
|
||
The resolution chat surface, the manual-resolve gesture, and how
|
||
the AI's replay attempts surface in the conversation are
|
||
unspecified. Narrow and concrete; defer-able until conflicts
|
||
happen often enough in real use to design against.
|
||
- **The pending-idea view's interaction design (remainder).** Topic
|
||
12 settled the decline ceremony per §9.3 (two-step composer-
|
||
then-preview dialog, "Propose a revised entry" affordance for
|
||
the declined proposer). The merge-confirmation ceremony and the
|
||
inline render of the meta-repo PR's diff against the catalog
|
||
remain unspecified and could earn their own topic.
|
||
- **The metadata pane UX.** §9.5 lands the metadata pane as a
|
||
structural commitment for super-draft title and tag edits. The
|
||
interaction design — when the pane opens, how multiple edits
|
||
queue into a single meta-repo PR, how AI assists tag editing —
|
||
is its own small UI topic.
|
||
- **The public face of discuss mode.** §8.14 settles what a
|
||
contributor sees in discuss mode on a branch with buffered
|
||
proposals. An anonymous reader or muted contributor on the same
|
||
branch is not yet specified — what they see of the pending
|
||
count, the preview disclosure, and the `Start Contributing`
|
||
invitation (which they can't act on) is its own small but
|
||
distinct topic.
|
||
- **Super-draft slug renames.** §9.5 defers renames on the basis
|
||
that a slug rename is a file rename in Git plus a cache-key
|
||
rewrite, rare enough to skip in v1. If usage shows real demand
|
||
— e.g., a super-draft whose framing shifted enough that the
|
||
original slug misleads — this earns its own topic, settling the
|
||
file-rename bot sequence, the redirect handling for any links
|
||
into the old slug, and the cache and threads migration.
|
||
- **Persistent accepted-change markup for returning contributors.**
|
||
§8.10 commits the editor's tracked-change markup to session-
|
||
local scope and points returning-contributor needs at DiffView.
|
||
A future session may revisit this with a per-user, per-branch
|
||
seen-cursor for accepted changes (mirroring §10.3's PR seen-
|
||
cursor) — markup persisting across reloads, dismissible with a
|
||
"mark as seen" gesture. Triggered by evidence of contributors
|
||
asking for it, not ahead of evidence.
|
||
- **AI participation as a notification source.** Topic 13 settled
|
||
that user-driven events carry the underlying user as actor and
|
||
system-generated events carry null. AI participant completions
|
||
(long-running change generation in a thread, an `Ask Claude to
|
||
propose a fix` invocation per §8.13 that returns minutes later)
|
||
are a third case the section did not explicitly settle — the
|
||
build will reveal whether they fire notifications at all,
|
||
whether they carry a synthetic "Claude" actor or fall under
|
||
null-system, and how the bot-vs-user distinction in §15.9
|
||
extends. Will surface during build if AI turn-times grow large
|
||
enough to warrant.
|
||
- **Cache bootstrap from a pre-existing meta repo.** §4.1 covers
|
||
steady-state cache freshness — the webhook is the fast path, the
|
||
reconciler the safety net — but assumes the cache grew up
|
||
alongside the bot. If the cache is rebuilt from scratch against
|
||
a meta repo that has history the bot did not author (a
|
||
transferred meta repo, a disaster-recovery rebuild after the app
|
||
database is lost), the reconciler has no `actions` rows to join
|
||
against for the §15.9 underlying-actor-not-bot resolution. The
|
||
Slice 1 build chose a fallback chain — audit log first, then
|
||
`On-behalf-of:` trailer parsing on the commit/PR body, then the
|
||
raw Gitea login — that is good enough for v1 but earns its own
|
||
topic once the cost of "the cache thinks the bot proposed
|
||
everything pre-app" becomes concrete. Touches §4.1 (the
|
||
reconciler's job description) and §15.9 (the attribution rule).
|
||
- **Branch-name path routing.** Slice 2's `branches/<branch>`
|
||
endpoints use FastAPI's default `{branch}` path-segment matcher,
|
||
which refuses slashes. The Slice 2 auto-generated branch name
|
||
steered around this with `<login>-draft-<hex>`, but a user who
|
||
renames to a slashed name will 404 on read. The fix is to convert
|
||
every `branches/<branch>` route to `{branch:path}` with the
|
||
understanding that ordering matters (more-specific routes like
|
||
`branches/main/promote-to-branch` must register first). Surfaced
|
||
by Slice 2's testing; defer-able until a user actually wants a
|
||
slashed branch name.
|
||
- **Markdown round-trip fidelity in the editor.** Slice 2's manual-
|
||
flush converts the Tiptap document to text via `editor.getText()`,
|
||
which discards markdown structure on round-trip (lists become
|
||
flat lines, headings lose their `#`, links collapse to their text
|
||
content). A faithful HTML-to-markdown serializer — or switching
|
||
the on-disk format to a structured one the editor owns natively
|
||
— earns its own session once usage shows where the loss bites.
|
||
Touches §8.6 (commit unit) and §8.11 (the manual-edit card's
|
||
diff fidelity).
|
||
- **The chat feed's per-thread filter affordances.** §8.12 commits a
|
||
top-of-chat disclosure that lists open threads with anchor previews
|
||
and per-thread filter affordances. Slice 2 wired the disclosure
|
||
counts; the filter that collapses the feed down to a single
|
||
thread, and the per-thread "Reply" affordance that posts back into
|
||
a specific thread from the unified feed, are the natural follow-on.
|
||
Small scope, defer-able until the feed grows busy enough to
|
||
warrant.
|
||
- **Cross-branch source-message labelling.** §8.14's data-model rule
|
||
permits a `changes` row whose `source_message_id` points at a
|
||
message in main's chat — the row's `branch_name` was mutated from
|
||
`main` to the new branch on promote-to-branch, but the message
|
||
reference stays. Slice 2's frontend doesn't yet label these as
|
||
"from a conversation on main" in the change panel; a small visual
|
||
treatment is the natural follow-on. Surfaced by §8.14's data path
|
||
going through Slice 2 for the first time.
|
||
- **The PR-page diff renderer.** Slice 3 ships a line-level
|
||
unified/split diff between branch and main RFC.md bodies,
|
||
computed client-side from the two strings via a small LCS pass.
|
||
Sufficient for the single-file v1 surface, but the §10.3
|
||
per-hunk seen-cursor accent — distinct from the file-level
|
||
accent Slice 3 wires — and the inline `<change>`-block
|
||
attribution from `changes.commit_sha` are the natural follow-on.
|
||
Earns a topic when a contributor's PR carries enough changes
|
||
that a reviewer wants to scope review to one hunk at a time.
|
||
Touches §10.3 (the per-hunk accent voice) and §10.4 (anchoring
|
||
review threads to specific hunks rather than free-text quotes).
|
||
- **The §10.2 modal's AI-drafted text when no provider is
|
||
configured.** Slice 3 falls back to a deterministic stub
|
||
(`Edits to <RFC title>` plus a character-count line) when the
|
||
app has no LLM provider. The fallback is functional but does
|
||
not produce spec-voice text. Per-RFC model availability (the
|
||
first §19.2 candidate, on the funder-role topic) will need to
|
||
settle the credential-delegation shape before this earns its
|
||
own topic; until then, the stub is the right shape for the
|
||
no-credential-available case.
|
||
- **§10.9 replay AI participation.** Slice 3 implements the
|
||
structural §10.9 path — fresh resolution branch off main, replay
|
||
the accepted changes whose `original` text still locates exactly
|
||
once, surface the rest as stale-pending changes on the
|
||
resolution branch — but does not yet invoke the AI participant
|
||
on the ambiguous conflicts to attempt a re-anchored proposal.
|
||
The contributor re-anchors manually for now. The "AI runs
|
||
against unambiguous conflicts" pass earns its own topic once
|
||
conflicts happen often enough to design against; the §19.2
|
||
"conflict-replay UX in detail" entry already names this.
|
||
- **PR title and description sync with Gitea.** Slice 3's
|
||
`POST /api/rfcs/.../prs/<n>/description` updates the cache row
|
||
but does not mirror the edit back to Gitea via the issues
|
||
endpoint. The PR page is the canonical surface for v1 and the
|
||
cache is its source of truth, so the divergence is fine within
|
||
the app — but anyone reading the PR directly on Gitea sees the
|
||
pre-edit text. A small follow-on that propagates the edit
|
||
through the bot wrapper closes the loop.
|
||
- **The §10.7 90-day deletion timer wiring.** Slice 3 lands the
|
||
PR-merged state and the read-only treatment but does not wire
|
||
the §12 hygiene timer that fires the deletion. Slice 8
|
||
("Hardening") owns the §12 30/90 timers as a whole; calling out
|
||
here so the dependency is explicit.
|
||
- **In-app merge for metadata PRs.** Slice 4's metadata pane opens
|
||
a meta-repo PR per §9.5; the merge surface for those PRs is the
|
||
Gitea web UI for now, because `api_prs.py`'s merge endpoint is
|
||
scoped to body-changing PRs (`rfc_branch` and `meta_body_edit`).
|
||
A small follow-on adds a `prs/<n>/merge`-shaped path that handles
|
||
`meta_metadata` kinds — likely a tiny variant since there's no
|
||
diff-rendered review surface to inherit. Defer-able until usage
|
||
shows admins finding the Gitea round-trip annoying.
|
||
- **Cache-rebuild discovery of meta-repo edit branches.** Slice 4's
|
||
`refresh_meta_branches` scans every meta-repo branch and filters
|
||
by name prefix (`edit-` / `edit/`) to discover super-draft edit
|
||
branches. The reconciler hits this on every sweep, so it's
|
||
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.
|
||
- **Body full-text search.** When the time comes.
|
||
|
||
Topic 13 (notifications) is settled and folded into §5 (the
|
||
notifications, watches, branch_chat_seen, notification_user_mutes,
|
||
notification_digests tables and the per-user notification
|
||
preference columns on users), §6.2 (the orthogonality clarification
|
||
against §15.8's notification mutes), §7.2 (the per-row catalog dot
|
||
for watched RFCs with unseen activity), §9.3 (the personal-direct
|
||
email channel for proposal-merged and proposal-declined events),
|
||
the new §15 (Notifications, in full), and §17 (the notification
|
||
endpoints — list, mark-read, stream, watch mutation, preferences,
|
||
quiet-hours, per-user mute, unsubscribe, bounce webhook).
|
||
|
||
### 19.3 Working agreement for the queue
|
||
|
||
Pre-build sessions ran on the queue agreement from prior versions
|
||
of this section: pick the next topic from §19.1, drive it to
|
||
decision, fold it in, update §19.1 and §19.2, hand off. The build
|
||
session and any sessions after it run on a modified shape:
|
||
|
||
1. The build session implements a slice of the spec. The spec is
|
||
the source of truth; the build does not extend it.
|
||
2. Where running code reveals the spec was wrong, the build
|
||
session corrects the spec in the appropriate numbered section
|
||
with a brief note explaining what running code revealed.
|
||
3. New design questions surfaced during the build that are not
|
||
resolvable as implementation details accumulate in §19.2 as
|
||
new candidate topics, in the same shape as the existing
|
||
entries.
|
||
4. Sessions after the build pick from §19.2 by user choice, run
|
||
on the original queue agreement (drive to decision, fold,
|
||
update §19.2), and need not be sequential — multiple §19.2
|
||
topics can be settled in any order the user prefers.
|