Roadmap item #13. Ships the frontend Amplitude SDK behind the v0.13.0
cookie/privacy consent gate. The analytics wrapper lives at
`frontend/src/lib/analytics.js` and exposes `track`, `identify`, and
`anonymize` over a stable nine-event taxonomy (Page Viewed, RFC
Viewed, User Signed In / Signed Out, RFC Proposed, PR Opened, Comment
Posted, Beta Access Requested, Admin Permission Decision). The
wrapper reads consent via `getConsent()` / `onConsentChange()` from
`frontend/src/lib/consent.js` (v0.13.0); the SDK module is
dynamically `import()`-ed only after `consent.analytics === true`,
and a later granted→denied flip calls `setOptOut(true)` so events
stop without a page reload. The Amplitude API key is read from
`VITE_AMPLITUDE_API_KEY` at build time; when unset the wrapper logs
one console warning and no-ops so dev environments keep working.
Wired into App.jsx (route-change Page Viewed + sign-in identify +
sign-out anonymize), Login.jsx (User Signed In with method =
otc/passcode/trust-device, Beta Access Requested on capture-profile
submit), ProposeModal.jsx (RFC Proposed), RFCView.jsx (RFC Viewed),
PRModal.jsx (PR Opened), RFCDiscussionPanel.jsx (Comment Posted with
surface=discussion), PRView.jsx (Comment Posted with surface=pr),
Admin.jsx (Admin Permission Decision with action=grant/revoke).
Event bodies carry only ids and enums — no titles, no comment
bodies, no names, no emails. The user binding passes only
`String(viewer.id)`.
Secret-vs-overlay binding caveat: Amplitude browser API keys are
visible in the shipped bundle via dev tools. Per the roadmap, the
key is still bound through `flotilla secret set` (rather than
`flotilla overlay set`) to keep all-keys-in-Secret-Manager
regularity for the OHM deployment; the CHANGELOG documents the
choice. Operator pre-deploy gesture (in the Upgrade steps block):
`pbpaste | ... ohm-rfc-app-flotilla secret set ohm-rfc-app
AMPLITUDE_API_KEY` — the wave-paused step before this release can
deploy.
No backend events ship in this release (Amplitude SaaS holds the
events); no schema migration; backend is unchanged. Migration slot
015 remains unused and available for the next minor that needs a
schema bump. New dependency: `@amplitude/analytics-browser`.
`VITE_AMPLITUDE_API_KEY` documented in `frontend/.env.example` with
the binding-choice caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships DOCS.md and the /docs route — a plain-prose translation of
SPEC.md for users, distinct from the binding spec. Originating need
was the admin-vs-owner role distinction on /admin/users (§6.1);
response is a single guide that covers the framework's user-facing
surfaces end-to-end (roles, proposing, branches, PRs, graduation,
notifications). Mirrors /philosophy: markdown file at repo root +
sibling backend loader + MarkdownPreview render. No schema migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a successful OTC sign-in, a contributor can set a passcode
(4-20 characters, bcrypt-hashed) and use email + passcode for
subsequent sign-ins. OTC remains the structural fallback: five
consecutive failed verifies lock the passcode path for 15 minutes
(HTTP 423), and a forgotten passcode is recovered by requesting a
fresh code. Migration 015_passcode.sql adds four nullable columns
to the users table; existing rows pass through as OTC-only and can
opt into a passcode from a new Sign-in tab in /settings/notifications.
The /login surface is extended to a five-step flow (email → either
passcode or OTC code → optional post-OTC passcode offer → optional
set-passcode). SPEC corrections per §19.3 rule 2: §6 names the
three auth paths, §14.1 documents the stepped login flow, §17 lists
the four new /auth/passcode/* endpoints, §19.2 surfaces four new
candidates and refreshes the cross-refs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v0.3.0 / v0.7.0 allowed_emails admission gate with an
admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 / §14.1 / §17).
Any valid email can sign in via OTC; a fresh user lands in
permission_state='pending' with a captured first/last/why profile,
and an admin grant flips them to 'granted' before write endpoints
accept them. Grandfathered users pass through the migration with
the column default 'granted' so existing contributors are unaffected.
The allowed_emails table stays in the schema as a fast-path bypass
pending v0.9.0's admin user-management page (item #7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap item #11. Ships the non-modal bottom-of-page cookie consent
banner, the default /privacy and /cookies policy pages, the
`cookie_consent` table + two §17 endpoints for server-side persistence,
the localStorage fallback for anonymous viewers, the /settings
"Privacy & cookies" tab for revisiting the choice, and the
`frontend/src/lib/consent.js` helper that roadmap item #13's analytics
SDK (v0.15.0) will gate against. No analytics SDK ships in this release
— the consent infrastructure goes in first so the gate is already in
place. Adds SPEC §14.5 / §14.6, lists two new endpoints in §17, names
the new table in §5, and surfaces four §19.2 candidates (content-repo
file vs env-var policy, GPC / DNT headers, i18n, item-#13 dependency).
Two new optional env vars (`VITE_PRIVACY_POLICY_URL`,
`VITE_COOKIES_POLICY_URL`) — defaults render the framework's stub
pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Gitea OAuth gesture as the primary human-auth path
(roadmap item #5, SPEC §6.2). Users sign in by entering their email,
receiving a six-digit code via the existing SMTP layer, and entering
the code on a two-step /login surface. The Gitea OAuth callback
remains functional during migration — the new UI links to it as a
fallback for users with active OAuth sessions or older invite paths
— and is scheduled for removal in a future release once OTC adoption
is universal. Existing users are linked by email on first OTC sign-
in (gitea_id preserved); new users are provisioned with NULL
gitea_id and rely on email as the identity key. The migration
introduces backend/migrations/012_otc.sql (otc_codes table + users
schema rebuild for nullable gitea_id and a partial unique index on
email), two new endpoints (POST /auth/otc/request, POST /auth/otc/verify),
bcrypt as a new backend dependency for code hashing, and 11 new
tests in test_otc_vertical.py covering the happy path, expired and
consumed and wrong codes, the per-email rate limit, the allowlist
gate, the OAuth-era link path, fresh provisioning, and prior-code
invalidation on re-request. No new secrets are required — the
existing SECRET_KEY signs sessions and bcrypt's per-row salt covers
the code hashes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep-the-edges hardening release (roadmap item #4). Audits every
write-shaped backend endpoint to confirm each one enforces an explicit
auth.require_contributor (or stricter) gate before doing state-changing
work; adds a regression test net (test_anon_offlimits_vertical.py, 12
tests, 66 assertions) so future endpoints can't quietly ship without a
gate. The only behaviour change: GET /api/rfcs/<slug>/graduate/progress
now requires auth.require_user since the step detail (repo name, PR
number, rollback steps) isn't part of the v0.3.0 anonymous-read contract.
No schema, no env, no dependency changes; operator action is rebuild +
restart per CHANGELOG §0.6.0 upgrade steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap item #3. An RFC's main view now carries a discussion surface
distinct from PR comments and from branch chat. The substrate is the
existing threads/thread_messages tables — rows with branch_name IS NULL
scope to the RFC's main view; the schema already permitted that shape,
v0.5.0 is the first build to write it. Five new endpoints under
/api/rfcs/<slug>/discussion/..., a new RFCDiscussionPanel right-column
component used when branchParam === main, SPEC §10.10 settling
discussion-vs-contribution, and §17 listing the new routes. Notification
routing reuses the existing chat_message_in_participated_thread /
chat_reply_to_my_message event kinds with branch_name=null on the
fan-out row; a distinct event_kind is a §19.2 candidate. Anonymous
viewers can read; writes require contributor — v0.6.0's item #4 will
harden adjacent gates. No schema migration; minor bump, no operator
action required beyond rebuild and restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an email allowlist (toggleable per deployment) that restricts
OAuth sign-in to listed emails while keeping read paths public.
Anonymous visitors now see the full app shell in read-only mode
instead of the §14.1 landing wall. Empty allowlist = gate off, so
deployments that don't enable it behave exactly as 0.2.3.
Also fixes single-finger scroll on /philosophy and other .chrome-pane
views on iOS Safari (.app: 100vh → 100dvh).
Renames deploy/nginx/rfc.wiggleverse.org.conf →
ohm.wiggleverse.org.conf to match the deployed-domain rename
(rfc.wiggleverse.org deprovisioned 2026-05-27).
See CHANGELOG.md for full details + upgrade steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an unauthenticated GET /api/health endpoint returning JSON
{version, status} for ops tooling. version is the running framework
version (read from VERSION at process start, cached as a module-level
constant in backend/app/health.py); status is "ok" with HTTP 200 in
v1, with the "degraded" / 503 path reserved in the response shape for
future degradation conditions.
The structural value is the version-match check: a deploy control
panel (flotilla, in particular) polls the endpoint after
`systemctl restart` reports active and verifies the returned version
equals the tag just deployed, catching the failure mode where a
restart did not pick up the new code.
Patch-shaped per SPEC.md §20.2 — no operator action required, no env
vars, no schema changes. The CHANGELOG carries one MAY-language step
naming the optional monitoring affordance.
SPEC.md §17 now lists the endpoint; the §19.2 candidate-topic entry
records the topic's settlement (version source = VERSION file at
import time; degraded = reserved v1 scaffold; CHANGELOG = patch shape
with MAY-language).
Tests: backend/tests/test_health.py (3 tests — 200/payload shape,
unauthenticated, version-matches-VERSION). Full suite 128/128 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Philosophy.jsx parsed PHILOSOPHY.md with bare marked + dangerouslySet
InnerHTML, so ```mermaid fences fell through as <pre> blocks. Swapped
to MarkdownPreview, which already carries the lazy mermaid loader +
SVG render path used by the RFC body view. Pre-existing gap, surfaced
when an OHM deployment authored a mermaid block in its PHILOSOPHY_PATH
override.
PATCH per SPEC §20.2 — additive, no operator action required beyond
the routine rebuild + restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PRView.jsx declared its threadsByKind useMemo after the early-return
guard for the loading state. First mount: pr is null, early return
fires, 14 hooks called. Second render: pr populated, execution
reaches the useMemo, 15 hooks — React #310, page goes blank.
Move the useMemo above the early returns and null-safe the
pr?.threads access so the hook count stays stable across renders.
Pre-existing bug in the post-Contribute-rewrite PR view; surfaced
in production by 0.2.0's graduation merge race fix enabling the
workflow that opens this code path. Patch per §20.2 — no operator
action required beyond rebuilding the frontend and restarting.
- Fix §13.3 step 4 race against Gitea's async mergeability
computation: gitea.wait_for_mergeable polls the mergeable
field before calling merge, and bot._merge_with_retry rides
out the transient 'Please try again later' response.
- Frontend now requires VITE_APP_NAME at build time
(frontend/vite.config.js fails the build if unset). Header
brand, landing H1, and browser title read the deployment's
configured name.
- Establish framework versioning: VERSION (canonical) +
frontend/package.json#version mirror; CHANGELOG.md as the
release log with RFC 2119 / 8174 normative-language
upgrade-steps; SPEC.md §20 as the binding policy; CLAUDE.md
capturing the separation-of-concerns rule; docs/DEPLOYMENTS.md
as the downstream operating manual; docs/QUICKSTART-VISION.md
staking the future one-command-deploy capability.
- Surgical fix to SPEC.md opening so the spec no longer asserts
OHM is the corpus the framework produces.
- SPEC.md §19.2 gains the 'Deployment-supplied subject framing'
candidate that drives the 0.3.0 release.
The Phase 3 preview-pane inline overlay and Phase 4 raw-pane gutter
accent together cover the review affordance DiffView was the interim
home for, so the toolbar toggle collapses and the surface goes.
- frontend/src/components/DiffView.jsx deleted.
- RFCView.jsx: drop the DiffView + marked imports, the reviewMode /
reviewHTML state, the toggleReviewMode callback, the "Review changes"
toolbar button, and the reviewMode ternary in render. editorEditable
simplifies to mode === 'contribute' && canContribute. File-header
docstring updated: "DiffView" → "preview-pane tracked-change overlay
(§8.10)".
- App.css: remove the .diff-view-wrapper and .diff-view-empty rules;
relabel the DiffView section header as ChangeTooltip (which lives on
as the Phase 3 overlay's hover affordance). Drop the dead .editor-
content .tiptap rules — Tiptap had no remaining consumers after
DiffView's deletion. The Contribute toolbar still renders the
accepted/pending hint + Phase 6 Chat toggle; only the Review-changes
button is removed.
- MarkdownPreview.jsx: comment updated to drop the DiffView mention
from the scoped-Marked-instance rationale.
- SPEC.md: §8.10 retitled "Tracked-change markup" (was "...and the
review-mode toggle"); the legacy-DiffView paragraph rewritten as a
one-sentence retirement note. §8.1, §8.15, §9.4, §9.5 references to
DiffView / review-mode cleaned up. §19.2's persistent-accepted-change
candidate updated to point at the preview-pane overlay rather than
DiffView.
No DiffView orphans remain (grep confirms .btn-review-toggle is still
the Discuss-mode "Show tracked changes" toggle, ChangeTooltip and
trackedOverlay carry historical comments only, PRView's §19.2 comment
is unrelated). Frontend reload shows no console errors.
Backend integration suite: 125 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Below 1280px the right-panel (chat + change-card panel) collapses into
an off-canvas drawer toggled from a Chat button in the editor toolbar.
Above 1280px the inline three-column layout is unchanged. Replaces the
Phase 2 narrow-viewport-banner stopgap.
- drawerOpen useState in RFCView; per-session, default closed; tied to
a .rfc-body.drawer-open class plus .right-panel.drawer-open and the
backdrop's data-open attribute.
- ChatDrawerToggleButton in each editor-toolbar branch (Contribute,
Discuss-with-accepted-changes, and a narrow-only fallback toolbar
for the default state). Badge mirrors change-panel-header: pending
AI changes + the buffered manual card. Hidden above 1280px via CSS.
- .right-panel becomes position: fixed; top: 48px; right: 0; width:
min(420px, 92vw); transform: translateX(100%) by default; slides in
with .drawer-open. Backdrop sibling with opacity transition handles
click-to-close.
- Escape closes the drawer when open (useEffect listener gated on
drawerOpen, so it's a no-op when closed).
- Toolbar gets padding-right shifted to drawer-width when the drawer
is open at narrow widths so the toggle stays clickable to close
(otherwise it'd sit under the open drawer).
- @media (min-width: 1280px) restores .right-panel to inline flex
(360px, position: static), hides the backdrop, hides the toggle
button, and hides the narrow-only toolbar fallback.
- Removes the .narrow-viewport-banner JSX + its CSS block + its
@media display:none rule.
SPEC.md §8.1 gains a paragraph noting the narrow-viewport drawer
collapse — explicit acknowledgment that the three-column shape adapts
below ~1280px without re-architecting around responsive primitives.
Verified in Vite preview sandbox at 1024px (drawer offscreen,
backdrop opacity 0, toggle visible) and 1440px (inline three-column,
drawer fixed-positioning reset, toggle hidden, backdrop display:none).
Backend was down this session (proxy 502 on /api/me) so the cumulative
Phase 2–5 end-to-end verification gap is still open; sandbox proof
only, as called out in the phase prompt.
Backend integration suite: 125 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/cleanup-open-human-model.sh is the one-off used on
2026-05-25 to hard-delete the exploratory open-human-model RFC from
wiggleverse/meta, reserving the RFC #1 slot for the eventual `human`
RFC. Kept in-tree as the audit artifact for that removal.
.claude/launch.json captures the frontend dev-server invocation
(port 5180, --strictPort) so contributors and tooling can launch
the same way. .gitignore now excludes .claude/settings.local.json,
which is per-machine permissions and not appropriate to share.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 5dbcac8 reframe established OHM as the corpus and Wiggleverse RFC
as the substrate. The landing page and the global header still read
"Wiggleverse RFCs," which puts the substrate's name in front of a
visitor when what they're actually visiting is OHM. Flip both surfaces.
- App header brand and Landing's H1 → "Open Human Model"
- Subtitle becomes "An open dictionary of the words humans and
machines need to agree on" — names the corpus, not the process tool
- Pitch paragraph reworded to lead with "the Open Human Model is the
corpus this framework produces," with first RFC defining "human"
- New .landing-attribution block at the foot of the deck: "OHM is run
by Ben on Wiggleverse RFC — the open-source standardization-process
software that powers it. Anyone can run their own collaboration on
the same substrate." Reader can now tell instance-vs-substrate at
a glance
The §8.11 manual-edit card was a stub — "{N} paragraphs edited
directly" plus a countdown and a Save-now button. The spec says the
card should grow one inline word-diff per touched paragraph as the
contributor types, in the same green/red register the Phase 3 accepted
overlay uses. Phase 5 lands that.
In RFCView.jsx, the 800ms-debounced handleEditorUpdate now computes a
per-paragraph token diff alongside the existing paragraph count and
stores both on manualPending. Baseline is the same Phase 4 gutter
source — originalSourceLinesRef.current, the last server-confirmed
body split on blank lines — so the card resets to empty the same
moment the gutter clears (accept/decline/manualFlush/branch-switch).
In ChangePanel.jsx, diffWords is exported (it was already the AI
card's inline-diff engine — token-level LCS over a whitespace-
preserving /(\\s+)/ split, ~30 lines, no runtime dep). The manual
card consumes the same tokens. Wholly-inserted paragraphs render as
all-insert blocks; wholly-deleted paragraphs as all-delete blocks.
Visual register is intentionally shared with Phase 3's preview
overlay: same #dcfce7/#166534 inserts, same #fee2e2/#991b1b
strike-through deletes. Selectors are scoped under .change-manual-diff
rather than reusing .markdown-preview .tracked-* since the card lives
outside the preview surface.
Pre-fancy stance, matching Phase 4's gutter: the diff is index-aligned
against the baseline, so adding a paragraph in the middle lights up
the rest of the doc. Tolerated as the "you've touched stuff below this
point" cue. An LCS-anchored future pass can fix it.
Verification gap matching Phases 2/3/4: backend was not running this
session, so the live RFCView → real-branch flow wasn't exercised.
Drove the Vite preview sandbox instead — mounted ChangePanel with a
hand-built diffs payload, confirmed three blocks render (mixed edit,
wholly-inserted, wholly-deleted), inserts/deletes carry the expected
computed colors, no console errors. Backend integration suite still
green (125 passed).
Restores the §8.10 paragraph-margin marker layer Phase 1 dropped when
Tiptap left, this time against the CM6 raw pane on the left half of
the Contribute split. The matching inline tracked-insert/tracked-
delete overlay Phase 3 shipped lives on the preview pane to the
right; the two visual layers answer different questions on the two
halves of the split — "did anything change in this region?" (gutter,
amber, scannable) vs. "what changed here?" (inline, green/red,
precise) — and are deliberately separate signals.
New file: frontend/src/components/diffGutterExtension.js. The
extension exposes a `setBaseline` StateEffect and a gutter that marks
every line whose text differs from the same-indexed line of the
baseline (the last server-confirmed branch body). Per-line, not
paragraph-block — CM6's natural unit; collapsing to paragraph ranges
is more spec-faithful but adds code, and the per-line stance is the
pre-fancy default. A TODO is left for a future paragraph-collapse
pass if the result reads noisy.
MarkdownSourceEditor.jsx changes:
- Install the gutter extension in the editor's extension list.
- Seed the baseline to `initialDoc` at construct time.
- In the existing `initialDoc`-watching effect, dispatch the doc
replacement AND a `setBaseline` effect in the SAME transaction so
there's no one-frame window where the new doc reads as "divergent"
against the old baseline. This carries through every server-
confirmed branch refresh that RFCView already wires (accept,
decline, manual flush, branch switch); no RFCView changes needed
because all four paths already re-push `initialDoc` after pulling
fresh state.
Design calls per the Phase 4 prompt's open list:
• Per-line marks, not paragraph-block ranges. Pre-fancy stance.
• Amber (#f59e0b) thin 3px vertical bar, distinct from the
preview pane's green-on-light / red-on-light tracked-change
inline overlay. Reads as "in-flight / not yet on the server."
• Baseline reset on every branchView refresh (accept / decline /
manual flush / branch switch), matching RFCView's existing
originalSourceLinesRef discipline. Gutter then reads as "what's
in the buffer but not on the server."
• No hover / no click. The inline overlay already carries the
click-to-card binding; the gutter is scannable only.
Known caveats kept deliberately:
• Insert/delete shifts. Adding a line in the middle shifts every
subsequent line's index and the naive compare lights up
everything below — tolerated as the honest "you've touched
stuff below this point" cue. A future LCS-anchored pass could
fix it; Phase 4 doesn't need to.
SPEC §8.10:
- First paragraph rewritten to name the CM6 raw pane as the gutter's
home and replace the stale "ProseMirror plugin" wording with the
CodeMirror gutter framing. Mirrors how Phase 3 named the preview
pane as the inline overlay's home.
- DiffView retirement paragraph adjusted: gutter and inline overlay
together (across the split) cover its review affordance, not
"both layers in the same surface" — the layers are deliberately on
different surfaces.
Verification:
- Vite preview sandbox eval — standalone CM6 mount, dispatch tests
across construct (no marks on identity), per-line edit (mark on
exactly the touched line, confirmed by Y-coordinate matching the
line-number gutter element), baseline reset clears, atomic
doc+baseline dispatch leaves zero marks (the RFCView accept-flow
path), insert-in-middle exhibits the expected cascade, and
computed-style proof for the amber bar (`#f59e0b`, 3px wide,
positioned left of the line-numbers gutter at x=21 vs x=24).
- Screenshot captures the bar on the touched line only.
- 125 backend integration tests still green.
- Live RFCView accept → branch refresh → gutter clear flow against a
real branch was not driven (backend not running this session,
carrying forward Phase 2/3's verification gap). The sandbox-level
proof covers the atomic dispatch correctness; the next session
with a backend up should drive that golden path before piling
Phase 5 work on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reintroduces the §8.10 inline tracked-change layer Phase 1 dropped when
Tiptap left, this time anchored to the rendered preview rather than a
writable editor. Each accepted change on the branch decorates the
preview DOM with a `<span class="tracked-insert">` at the proposed text
and a `<span class="tracked-delete">` strikethrough for the original;
hover surfaces the same ChangeTooltip DiffView uses (badge + prompt +
quote + reason), now extracted to its own file so both surfaces share
the affordance.
Design calls per the Phase 3 prompt's open list:
• Contribute pane: default-on. The pane exists for editorial review
of your own work; the overlay reinforces that.
• Discuss pane: opt-in via a new "Show tracked changes" toolbar
toggle on non-main branches. Clean prose stays the default.
• DiffView's "Review changes" toolbar is untouched — DiffView dies in
Phase 7 and overloading its toggle now creates surface to unwind.
Pre-fancy stance on the known overlay subtleties:
• Drift — if `proposed` no longer appears verbatim (later manual
edit touched it) we skip the decoration rather than mis-anchor.
• Overlap — decorate in `acted_at` ascending order; later spans win
on whatever text is still un-wrapped.
• Mermaid — text nodes inside `.mermaid-block` are skipped; a tracked
span that intersects diagram source drops silently. Flagged as a
known limitation rather than worked around.
• Code — `<pre>`/`<code>` parents skipped too; matching inside
verbatim code produced noisy false positives in fixtures.
Backend tests: 125 passed. Helper verified via Vite preview sandbox
eval across eight cases (single-paragraph match, distinct-original
strike, no-match skip, mermaid skip, two-change overlap ordering,
declined/pending ignored, whitespace-normalized cross-newline match,
code-block skip), plus computed-style proof for the new
`.markdown-preview .tracked-insert` / `.tracked-delete` rules. The
end-to-end CM6 → accept → overlay flow against a live branch wasn't
exercised (backend not running this session); the simpler unit-level
verification looked clean, but a future session with the backend up
should drive that golden path before Phase 4 piles on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SPEC, DEV docs, and code comments still talked about the codebase as
a rewrite-in-progress against an external prototype. With v1 shipped
the framing reads oddly — it implies code is provisional when it's
the production thing. Recast §18 as "the technical stack," strip
"carryover from the prototype" comments across backend (api.py,
chat.py, providers.py) and frontend (DiffView, PromptBar,
SelectionTooltip, modelStyles), and rework SPEC §1 / §18 to introduce
OHM up front rather than as a follow-on to a prototype reference.
Also:
- RUNBOOK: bump Python prereq to 3.11+ to match the production VM
(was 3.13).
- Remove IMPLEMENTATION-PROMPT.md — the original implementation brief
is no longer load-bearing.
- Add deploy/DEPLOY-NEW-SESSION-PROMPT.md as the durable
deploy-handoff prompt for new sessions.
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>
Reframe the Open Human Model in SPEC.md and PHILOSOPHY.md from "the
first RFC the framework will produce" to "the corpus of RFCs the
framework produces, of which the first defines *human*." Earlier
phrasing collapsed the project (OHM) and the first entry into one
name; this teases them apart.
Also surface the OpenXML APIs / UX downstream-consumer point: OHM is
English-first by design — the markdown bodies are canonical, and the
structured artifacts downstream systems need to actually let humans
and machines interact are derived from that English source, not
authored alongside it. This is part of why markdown round-trip
fidelity matters structurally (cf. the Phase 1 CM6 swap).
Updates the obvious example renames — slug `open-human-model` →
`human`, title "Open Human Model" → "Human", PR-list / breadcrumb /
notification examples — so the SPEC's worked-example consistently
shows OHM-as-corpus with Human as a member. Test fixtures and the
README seed-script invocation still carry the old slug; those are
left for a separate pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a
CodeMirror 6 markdown source editor. Discuss mode and any read-only
viewing continues to render through Tiptap.
The §8.11 manual-edit debounce now reads the raw markdown source via
CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip
that RFCView.jsx flagged as a §19.2 candidate; what the contributor
typed is exactly what gets POSTed to manual flush.
The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was
dead in read-only Discuss anyway, and Phase 4 of the Contribute
rewrite adds a change-anchored gutter against the CM6 raw pane.
The Tiptap-side accept-time injection of <span class="tracked-*">
markup also drops out — CM6 can't render those spans, and Phase 3's
preview pane is the proper home for tracked changes. The reviewMode /
DiffView toggle still works against marked-rendered HTML in the
interim until Phase 7 retires it.
Notes:
- Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/
language/lang-markdown). Mermaid in Phase 2 will be the larger lift
and should lazy-load.
- Verified the CM6 editor mounts cleanly via Vite dev preview: ref
handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc
changes, gutter / line-numbers / active-line all render, and the
source round-trips verbatim. Could not drive the full RFCView
Contribute flow end-to-end without a running backend; the manual
countdown + save-now + accept/decline pathways are verified by
inspection only.
- 125 backend integration tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second §19.2 settlement after v1. New §6.7 alongside §6.6: optional
`funder:` frontmatter field names a single gitea_login; a
`funder_consents` app-db row records funder-side opt-in; both halves
required for the binding to activate (two-key rule). Funder universe
replaces — does not augment — the operator universe per-RFC for
attribution-clean resolution. Funder role grants zero §6.1/§6.3
authority. Three revocation paths each restore the operator-credentials
status quo.
§19.2's credential-delegation entry is split: lighter half marked
settled with a pointer to §6.7; operational-realities half (mid-call
failure, rotation, billing, rate-limit attribution) lives on as its
own entry. Test suite is 125/125 green (106 prior + 19 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First §19.2 candidate settled after v1. The heavier per-RFC-model
topic subdivided into UX (this) and credential delegation + funder
role (still §19.2). New §6.6 carries the rule: an optional `models:`
frontmatter field on the meta-repo RFC entry; absent inherits the
operator universe, populated narrows the picker to the intersection
with provisioned providers, `[]` opts the RFC out of AI entirely.
The first resolved entry is the RFC default. §18's ENABLED_MODELS is
reframed as the operator universe.
Code: migration 009 adds nullable cached_rfcs.models_json (NULL ≠ []
is load-bearing); entry.py grows the optional field with absent-vs-
empty round-tripping in parse/serialize; new models_resolver module
holds the rule; api_branches replaces /api/models with the slug-aware
/api/rfcs/{slug}/models and threads the chat + reask paths through
the resolver; api_prs §10.2 uses the resolver and extends the stub
fallback to the opt-out case; frontend passes slug to listModels.
Tests 106/106 green (96 prior + 10 in test_per_rfc_models.py). No
behavioral change for entries without `models:` — operator universe
preserved as default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Five new integration test files raise the suite from 75 to 96 green:
test_hygiene_vertical (7), test_branch_path_routing (4),
test_metadata_pr_merge (3), test_cache_bootstrap (4), test_e2e_smoke
(3). The smoke test walks propose → super-draft → edit branch →
body-edit PR → graduate → active-RFC PR → merge → notification →
hygiene-sweep deletion end-to-end.
- deploy/RUNBOOK.md replaces the prior DEPLOY.md stub as a real
runbook: prerequisites, first-time bring-up, day-2 ops (logs, DB
backup, secret rotation, the §12 hygiene cadence), rollback shape,
troubleshooting table.
- backend/.env.example grows the SMTP block, HYGIENE_TICK_SECONDS,
and WEBHOOK_EMAIL_BOUNCE_SECRET with inline commentary.
- README points to RUNBOOK.md; the "what the build lets you do"
section adds Slices 7 and 8.
- docs/DEV.md gets a Slice 8 — shipped section; the "Next slice"
footer becomes the v1-complete epitaph.
- SPEC corrections per the §19.3 working agreement: §10.7 names the
shared §12 sweep; §12 names the bot as actuator and the per-user
branch_chat_seen preservation contract; §19.1 marks v1 complete
and records Slice 8; the five §19.2 candidates Slice 8 folded in
are marked settled with pointers at the resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
DigestScheduler shape; wires next to digest in main.py with the
same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
api_prs.py to {branch:path}; move the bare GET to the bottom of
the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
slashed-branch read; integration tests for the hygiene vertical
cover the 30d close, 90d delete, post-merge delete, pinned
exemption, per-user cursor preservation, no-notification rule,
and the graduation-rollback cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
§14.1 richer landing, §14.2 /philosophy route (disk-backed), §14.3
persistent About link. /settings/notifications surfaces Slice 6's
preferences/quiet-hours/mute/watches endpoints. /admin home base
consolidates role management, the §6.2 write-mute, the audit-log
viewer, the permission-events log, and the §13.2 graduation queue.
Backend: backend/app/philosophy.py, backend/app/api_admin.py (seven
admin endpoints + user-search), GET /api/users/me/notification-mutes.
Frontend: Landing.jsx (deck), Philosophy.jsx, NotificationSettings.jsx,
Admin.jsx, App.jsx routing for the chrome surfaces.
Tests: backend/tests/test_chrome_vertical.py — 13 cases. Full suite
75/75 green.
Spec corrections: §14.2 (PHILOSOPHY.md source is a deployment-time
decision), §17 (admin block extended to name the seven new endpoints
+ user-search and notification-mutes read). §19.1 rewritten for
Slice 8 hardening; §19.2 grew four candidates (owner succession,
mute-from-actor, the "Following since <date>" disclosure, audit-log
row prose).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The §13.3 transactional sequence flips a super-draft to active —
five steps with paired undoes, an in-process orchestrator fed by
an asyncio.Queue, the §17 SSE endpoint streaming step transitions
to the dialog. Each step is a new bot primitive that logs an
`actions` row, bracketed by `graduate_start` / `graduate_complete`
for the linkable audit sequence. Rollback runs the undoes in
reverse from the last completed step; merge_pr has no undo by
design per §13.5.
The §9.8 precondition gate is enforced server-side at the top of
POST /graduate so the §13.3 rollback complexity does not grow.
The §13.4 chat migration is a database semantic no-op — the
(slug, branch_name='main') threads keep their identity, only the
interpretation changes. The §9.8 pre-graduation history surfaces
via a new _is_meta_target(rfc, branch) dispatch helper and lands
as pre_graduation_history on /main.
§13.1 claim flow landed alongside since it's the prerequisite for
non-admin graduation — bot.open_claim_pr plus broadening
api_prs._require_pr to accept meta_claim.
45/45 tests green; ten new integration tests cover the validator,
the §9.8 precondition refusal, happy path with audit verification,
mid-sequence rollback at steps 2 and 3, concurrent refusal,
chat-survives-without-data-movement, pre-graduation history, and
the §13.1 claim PR cycle.
SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew
four candidates surfaced during the slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Single-host deployment of the app at rfc.wiggleverse.org alongside
the existing Gitea instance. nginx reverse-proxies /api/* and
/auth/* to a single uvicorn process on 127.0.0.1:8000 and serves
the Vite build output as static files; certbot adds the TLS cert
in place; systemd supervises the process per §4.2's
single-process-with-WAL-SQLite contract (one worker; raising
--workers would break the invariant).
deploy/DEPLOY.md is the step-by-step runbook covering host prep,
Gitea bot + OAuth setup, .env shape, meta-repo seed, nginx +
certbot, systemd, smoke test, and the update/rollback shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the license on the rfc-app-prototype that this codebase
inherits its §18 carryovers from. PHILOSOPHY.md frames the framework
as public infrastructure; MIT is the lowest-friction license for
that posture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 5 will own the §13 graduation flow that creates per-RFC repos
from super-drafts; until then, the §8 active-RFC view needs a dev
shortcut to bring an `state: active` entry plus a per-RFC repo into
existence. The script:
- Creates `<org>/rfc-NNNN-<slug>` via bot.ensure_rfc_repo_seed,
seeding RFC.md on main.
- Registers the §4.1 webhook on the per-RFC repo.
- Creates (or graduates) the meta-repo entry with state=active,
id=RFC-NNNN, repo=<full>.
Requires the operator's gitea_login to have a `users` row (sign in
once via OAuth first); refuses to synthesize a fake user since the
§6 / §15.9 attribution surfaces read from it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.
Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.
Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.
Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the
§5 schema (six numbered migrations), Gitea OAuth + §6 user
provisioning, the §7 catalog left pane, and the propose-to-merge
vertical: propose modal opens an idea PR against the meta repo, an
owner merges from the pending-idea view, the cache picks it up via
webhook or reconciler sweep, and the catalog renders the new
super-draft.
Per §1 the bot is the only Git writer; every commit, branch
creation, and PR merge carries the §6.5 On-behalf-of: trailer and
an `actions` audit row. Per §4 the cache is never written from a
user action — it's webhook+reconciler only.
Covered by `backend/tests/test_propose_vertical.py` against an
in-process Gitea simulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>