Ben Stull 7c3b8fc133 Contribute rewrite Phase 2: split-pane preview with mermaid
Add a rendered preview pane and split the Contribute-mode center
column. Discuss mode is now a single rendered preview (no more
read-only Tiptap mount); Contribute mode renders the CM6 raw editor
on the left and a live-updating preview on the right at 50/50, with
the existing chat + change-card panels untouched on the right.

The new MarkdownPreview component renders markdown via marked and
lazy-loads mermaid on the first encounter of a ```mermaid fence in
any rendered doc. Mermaid (~200 KB gzipped, plus dagre/graphlib
subchunks) stays out of the main bundle and is fetched only when a
diagram actually appears in the rendered content. The marked
renderer is scoped via a per-component Marked instance so the
mermaid-fence behavior does not leak to Editor.jsx / DiffView.

XSS hardening on mermaid: securityLevel: 'strict' is set explicitly
in mermaid.initialize. Verified end-to-end via a sandbox eval — a
hostile `<script>window.X=true</script>` payload inside a mermaid
fence is neutralized (no script tags in the rendered SVG, no global
side-effect, diagram still renders with the offending node label
empty).

The §8.12 selection tooltip is now sourced from window.getSelection()
inside the preview surface rather than Tiptap PM positions. The
SelectionTooltip's existing `{text, coords}` contract is unchanged;
coords come from the selection range's bounding rect. Anchor payloads
for flag threads now carry just the quote text — the PM-position
from/to fields that the Tiptap-era code attached are dropped (they
were never meaningful as durable anchors anyway; quote is what §8.12
and §8.13 specify).

Design decisions confirmed before coding (all defaults):
- PromptBar spans both panes at the bottom of the center column; it
  operates on the document, not on either pane individually.
- Start-Contributing is a hard cut — no transition animation. Phase
  6's chat-drawer collapse will redo the layout machinery anyway.
- Mermaid lazy-loads on first ```mermaid fence detected, not on
  Contribute-mode entry. Keeps the gzipped cost off any doc that has
  no diagrams.
- Below 1280px viewport, a narrow-viewport banner surfaces in
  Contribute mode. The drawer collapse that fixes this properly is
  Phase 6.

Mermaid integration is kept loose for the future authoring tool: the
placeholder DOM node carries the source as a data attribute, the
renderer takes (source) → SVG via mermaid.render, and per-block
memoization keys on source. A future authoring pane can intercept
the placeholder before SVG render without restructuring.

SPEC §8.3 updated to reflect the split-pane Contribute layout — the
smallest edit that captures the new shape.

Verification notes:
- Vite preview on :5180 — sandbox mount of MarkdownPreview confirms
  rendering, mermaid SVG output, securityLevel:'strict' XSS
  neutralization, and the window.getSelection → {text, coords}
  bridge end to end.
- Network log confirms mermaid + sub-chunks are absent from initial
  load and fetched only after first ```mermaid encounter.
- Backend was not running this session, so the manual-debounce
  save-now / accept / decline / live-preview-mirror pathways were
  not driven against a real branch; they remain verified by
  inspection. Phase 1's verification gap therefore carries forward.
- 125 backend integration tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:24:19 -07:00
2026-05-24 05:10:50 -07:00

RFC App

A single-process FastAPI + SQLite + React + Vite + Tiptap app that materializes the Wiggleverse RFC framework specified in SPEC.md. The framework's mission lives in PHILOSOPHY.md; the spec is the binding contract; this README is how to bring the app up against a local Gitea instance and exercise the surface the build has shipped.

The v1 build is complete. Subsequent sessions pick from §19.2 by user choice per §19.3's working agreement. See docs/DEV.md for the build history and the work mode that follows v1.

What the app expects to talk to

  • A Gitea instance at GITEA_URL. The instance hosts the meta repository and (eventually) one repository per graduated RFC.
  • A bot service account in that Gitea, with a personal access token in GITEA_BOT_TOKEN. Per §1 the bot is the only writer in the system — every commit, branch, and PR the app produces flows through one wrapper that applies the §6.5 On-behalf-of: trailer and records a row in the actions audit log.
  • An OAuth2 application registered against that Gitea, with the callback URL set to {APP_URL}/auth/callback. Real human users authenticate via Gitea OAuth (the §18 carryover); the app reads their Gitea profile, provisions a row in users, and layers §6's app-owned permission model on top.

Local bring-up

The shortest path from a clean checkout to a working app is:

1. Stand up a local Gitea

Anything that exposes the Gitea REST API works. The fastest path is Docker:

docker run -d --name gitea \
  -p 3000:3000 -p 222:22 \
  -v gitea-data:/data \
  gitea/gitea:1.21

Open http://localhost:3000, walk through the install wizard (SQLite, default port), and create your owner-zero account.

2. Create the bot service account

In Gitea, sign in as your owner account and Site Administration → User Accounts → Create User Account. Give it a name like rfc-bot and an email. Then sign in as the bot, open Settings → Applications → Generate New Token, and grant it the write:repository, write:user, and write:admin scopes (admin is needed because the bot will create per-RFC repos on graduation; in v1 you can scope down to repo and org if you want to defer admin until Slice 5).

Copy the token; you will paste it into .env.

3. Create the org that will host the meta repo

The seed script creates the meta repo inside an org. Create the org (e.g. wiggleverse) in Gitea and add rfc-bot to it as an Owner.

4. Register the OAuth2 application

In Gitea: Site Administration → Integrations → OAuth2 Applications → Create. Name it whatever you like, set the redirect URI to http://localhost:8000/auth/callback. Copy the client id and client secret — they go into .env.

5. Configure the app

cd backend
cp .env.example .env
$EDITOR .env    # fill in every variable

Required values:

Variable What it is
GITEA_URL Base URL of the Gitea instance (no trailing slash).
GITEA_BOT_USER The bot account's login.
GITEA_BOT_TOKEN The bot account's access token.
GITEA_ORG The org that owns the meta repo.
META_REPO The meta repo's name (default meta).
OAUTH_CLIENT_ID From the OAuth app you registered.
OAUTH_CLIENT_SECRET Likewise.
APP_URL The URL the app is reachable at locally.
SECRET_KEY A long random string for cookie signing.
OWNER_GITEA_LOGIN Your owner-zero Gitea login — gets the owner role on first sign-in.
GITEA_WEBHOOK_SECRET A shared secret for the §4.1 webhook signature.

Optional values, picked up at process start:

Variable What it is
ENABLED_MODELS Comma-separated provider keys for §18 chat (e.g. claude,gemini).
ANTHROPIC_API_KEY etc. Per-provider keys; missing keys disable that provider.
SMTP_HOST / SMTP_PORT §15.4 transactional-email adapter target. Empty falls back to logging the envelope to stdout — sufficient for dev and integration tests.
SMTP_USER / SMTP_PASSWORD SMTP auth credentials. Optional alongside SMTP_HOST.
SMTP_STARTTLS 1 (default) to negotiate STARTTLS; 0 for plaintext.
EMAIL_FROM Envelope From address for §15.4 mail. Defaults to a non-routable placeholder.
EMAIL_FROM_NAME Display name on the From header (default Wiggleverse).
EMAIL_ENABLED 1 (default) to dispatch email; 0 to suppress all sends without disabling the inbox.
EMAIL_BUNDLE_THRESHOLD Held-during-quiet-hours threshold for the "Activity while you were away" bundle (default 5, §15.4).
DIGEST_TICK_SECONDS Cadence of the §15.5 digest scheduler's loop (default 3600). Tests drive ticks synchronously via digest.run_tick.
HYGIENE_TICK_SECONDS Cadence of the §12 hygiene scheduler's loop (default 3600). Tests drive ticks via hygiene.run_tick(now=...).
WEBHOOK_EMAIL_BOUNCE_SECRET When set, /api/webhooks/email-bounce requires the same value in the X-Webhook-Secret header. Unset leaves the webhook open for dev — the v1 contract.

6. Install dependencies

Backend:

cd backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Frontend:

cd ../frontend
npm install

7. Seed the meta repo

The seed script creates wiggleverse/meta if it does not exist, populates it with PHILOSOPHY.md, README.md, CONTRIBUTING.md, the regenerate-index workflow placeholder, and an empty rfcs/ directory, and registers the Gitea webhook the app needs:

cd backend
.venv/bin/python ../scripts/seed_meta_repo.py

Re-running is safe — every step is upsert-shaped.

8. Run the app

In two terminals:

# Terminal 1 — backend
cd backend
.venv/bin/uvicorn app.main:app --reload --port 8000
# Terminal 2 — frontend
cd frontend
npm run dev

Open http://localhost:5173. Sign in with your owner-zero Gitea account. The catalog should appear empty; the + Propose New RFC button at the bottom opens the propose modal.

What the build lets you do

Slices 18 are shipped — v1 is complete. End-to-end paths the app supports:

  • Propose → idea PR → merge → super-draft (Slice 1, §9.1–§9.3).
  • Super-draft body editing via meta-repo edit branches, with AI participation, the change-card panel, manual flushes, threads, flags, and DiffView (Slice 4, §9.5–§9.7 + §8 inherited).
  • The §8 active-RFC view in full: per-branch chat, AI participation through the <change> protocol, accept / decline / edit, manual-edit flushes, sub-threads, flags, DiffView (Slice 2, §8 in full).
  • The §10 PR flow against both per-RFC repos and meta-repo edit branches: open, AI-drafted title and description, the §10.3 review page with the per-user seen-cursor, review threads, merge, withdraw, the §10.9 conflict-replay path (Slice 3 + Slice 4's routing-collapse extension, §10 in full).
  • §13 graduation with the three-field dialog, the precondition popover for blocking body-edit PRs, the SSE-streamed five-step sequence, rollback on mid-sequence failure, and the §9.8 pre-graduation history affordance on the new RFC view (Slice 5, §13 in full).
  • §13.1 ownership claim as a meta-repo PR adding the claimant to the entry's owners: field; admin/owner merges the PR (Slice 5).
  • §15 notifications end-to-end: a producer-side chokepoint in notify.py fans out from actions (and from chat-message inserts that don't go through the bot) into notifications rows under §15.1's routing rules; §15.6 watches auto-set on the first substantive gesture and decay after 90 days; the header badge and the /inbox overlay back the live counter via an SSE stream per §15.3; toasts fire for personal-direct events and for events landing on the view the user is currently watching; §15.4 email opts in per category with one-click unsubscribe and a global opt-out wired to the bounce webhook; §15.5 weekly / daily digest assembles eligible churn into a single mail; §15.7 reconciles unread state when a scope cursor advances; §15.8 quiet hours hold email and digest while letting the inbox row still land, and the per-user mute suppresses inbox rows produced by a specific actor (Slice 6).
  • §14 chrome and the settings / admin neighbourhoods — the landing page with the three-item deck, the /philosophy route reading PHILOSOPHY.md from disk, the persistent About header link, /settings/notifications exposing the five §15-derived sub-sections, and /admin as the four-tab home base for role management, write-mute, audit-log, graduation-queue, and the §6.5 permission-events read (Slice 7).
  • §12 branch hygiene + §10.7 post-merge deletion — the 30/90-day timers ride on a scheduler next to the digest, the bot is the only Git writer per §1, and the audit-log rows surface as "the app" actor per §15.9. The §19.2 candidates the hardening pass folded in: cache bootstrap against a meta repo the bot did not author, branch-name path routing via {branch:path}, in-app merge for meta_metadata PRs, the graduation-rollback branch cleanup, and the email-bounce webhook signing seam (Slice 8).

This exercises the §4 cache (webhook + reconciler), the §6 permission model in full, the §1 bot wrapper (every Git write goes through it, every commit and PR carries the On-behalf-of: trailer), the §17 routing-collapse rule that lets active and super-draft surfaces share their endpoints, and three scheduled jobs in the same shape (reconciler, digest, hygiene).

Out of scope for v1: every item under §16 ("What is deliberately deferred"), and the §19.2 candidates the hardening pass left queued. Subsequent sessions pick from §19.2 by user choice per §19.3's working agreement.

Verifying it worked

After bring-up:

  • http://localhost:8000/docs lists the API routes the build session has wired so far.
  • sqlite3 backend/data/rfc-app.db .schema shows the §5 schema.
  • gitea ls /api/v1/repos/wiggleverse/meta/contents/rfcs after a proposal merges should show one new <slug>.md.

Seeding an active RFC for §8 testing

With Slice 5 shipped, the /graduate flow in the app is the canonical path from super-draft to active. The scripts/seed_test_rfc.py shortcut is still around for dev sessions that want an active RFC without running the §9.1 propose flow and the §13 graduation dialog by hand. Sign in once via OAuth so a users row exists, then:

cd backend && .venv/bin/python ../scripts/seed_test_rfc.py \
    --slug open-human-model \
    --title "Open Human Model"

The script creates wiggleverse/rfc-NNNN-<slug>, seeds RFC.md on main, registers the webhook, and graduates the meta entry as a bootstrap-only direct write. The §8 surface at /rfc/<slug> then has something real to render.

Troubleshooting

  • The catalog stays empty after a merge. Check that the webhook is reaching the app: the reconciler runs every five minutes and will catch up, but a missing or misconfigured webhook is the most common reason for sub-second freshness to fail. The seed script registers the webhook for you; if you bring up Gitea on a different host (e.g. a Codespace, a tunnel), re-run the seed against the new APP_URL.
  • OAuth callback errors. The redirect URI in Gitea has to match APP_URL exactly, including the protocol and port.
  • The bot can't merge. The bot needs Maintainer or Owner on the meta repo (membership in GITEA_ORG as Owner gives it both).

Where to read further

  • SPEC.md — the binding contract. Every load-bearing decision is there.
  • PHILOSOPHY.md — why this framework exists. The spec's decisions answer to it.
  • docs/DEV.md — the build's slicing plan, the current state, and the next slice's brief.
  • deploy/RUNBOOK.md — single-host production bring-up, day-2 operations (logs, database backup, secret rotation, the §12 hygiene cadence), and rollback shape.
S
Description
Open-source RFC platform software
Readme 589 KiB
Languages
Python 66.2%
JavaScript 21.9%
HTML 5.9%
CSS 5.8%
Shell 0.2%