Release 0.14.0: public /docs user guide (DOCS.md served at /docs)

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>
This commit is contained in:
Ben Stull
2026-05-28 03:08:28 -07:00
parent 55beba5c0a
commit de28272914
9 changed files with 821 additions and 2 deletions
+73
View File
@@ -23,6 +23,79 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond
that.
## 0.14.0 — 2026-05-28
**Minor — no operator action required; new optional env var.** This
release ships `DOCS.md` and the `/docs` route — a public-facing user
guide that translates `SPEC.md` into plain prose for readers,
proposers, and contributors. The originating need was the
admin-vs-owner distinction on the `/admin/users` surface (the §6.1
role separation was load-bearing but only documented in spec voice);
the response was a single guide that covers the framework's user-
facing surfaces end-to-end. Mirrors `/philosophy` end-to-end: a
markdown file checked into the repo root, served by a sibling backend
loader, rendered with `MarkdownPreview`. No schema migration. No
required env-var changes. The new "Docs" header link sits alongside
the persistent "About" link from §14.3 and is reachable by anonymous
viewers per the same v0.3.0 anonymous-read contract.
### Added
- **`DOCS.md`** at the repo root — the user-facing guide. Covers
reading anonymously, signing in, proposing an RFC, super-drafts vs
active RFCs, the discussion-vs-contribution distinction (§10.10),
working on a branch (contribute mode, AI proposals, manual edits,
flags, branch visibility, contribute grants, hygiene), opening and
reviewing PRs, graduation (§13), withdrawal and reopening, the AI
participant (§6.6 / §6.7 / §18), notifications and watch states
(§15), and the full roles-and-permissions story (§6 in plain
prose: anonymous / contributor / admin / owner, per-RFC
owners + arbiters, per-branch contribute grants, the write-mute,
and the three structurally distinct "mutes"). Framework-neutral —
no deployment-specific names or corpus references; consistent with
`CLAUDE.md`'s separation-of-concerns rule.
- **`backend/app/docs.py`** — sibling loader for `philosophy.py`.
Reads `DOCS.md` from the repo root with the same disk-first,
in-process-cached, `refresh()`-on-demand shape. Optional
`DOCS_PATH` env var points at an alternative source (e.g. a
meta-repo working-tree clone) for deployments that prefer that.
- **`§17` endpoint** — `GET /api/docs` returns
`{ "body": "<DOCS.md verbatim>" }`. Anonymous-reachable, same
contract as `GET /api/philosophy`.
- **`frontend/src/components/Docs.jsx`** — the `/docs` reading
surface. Mirrors `Philosophy.jsx`: chrome with Back / "USER GUIDE" /
Home affordances, body rendered through `MarkdownPreview`.
### Changed
- **`backend/app/api.py`** — imports `docs as docs_mod` alongside
`philosophy` in the relative-import block; registers the new
`GET /api/docs` handler immediately after `GET /api/philosophy`.
- **`frontend/src/api.js`** — exports `getDocs()` alongside
`getPhilosophy()`. Same fetch shape, different endpoint path.
- **`frontend/src/App.jsx`** — imports `Docs` alongside `Philosophy`,
registers the `/docs` route alongside `/philosophy`, adds the
persistent "Docs" header link alongside "About", and adds the
`DocsWithSidebar` chrome wrapper alongside `PhilosophyWithSidebar`.
### Upgrade steps (from 0.13.0)
- You **MUST** rebuild the frontend and restart the backend after
upgrading so the new `/docs` route, the new endpoint, and the new
loader are picked up. `frontend/package.json#version` and `VERSION`
both move to `0.14.0`. No schema migration; the new endpoint
serves a checked-in file.
- You **MAY** set `DOCS_PATH` to an absolute path if your deployment
hosts `DOCS.md` outside the framework's repo (e.g. as a sync target
from a content repo). Unset is supported — the framework's
`DOCS.md` at the repo root is the default, mirroring how
`PHILOSOPHY_PATH` works for `/api/philosophy`.
- You **MAY** customize `DOCS.md` for your deployment if you want
deployment-specific phrasing layered on top of the framework's
guide. The file is a regular markdown source; standard `vim`/`git`
edits suffice. Framework upgrades that ship a new `DOCS.md` will
show as a normal merge in your deployment-overlay layer.
## 0.13.0 — 2026-05-28
**Minor — schema migration required; new optional env vars.** This
+605
View File
@@ -0,0 +1,605 @@
# Using the RFC app
This is the user-facing guide to the Wiggleverse RFC framework — how to
read what's here, propose a new RFC, contribute to one that already
exists, and understand who is allowed to do what.
This guide describes the framework. Individual deployments brand and
configure themselves independently — the name in the header and the
corpus the RFCs are about belong to the deployment, not to this
document.
For the *why* of the framework, read the [philosophy](/philosophy).
For the binding technical contract, see `SPEC.md` in the repository.
---
## Reading without signing in
You can read the catalog and every public RFC without an account.
Anonymous visitors can:
- Browse the catalog of super-drafts and active RFCs.
- Open any RFC and read its canonical body.
- Read any public branch — its diff and its chat thread.
- Read any pull request — its diff, its conversation, its review
comments.
- Read the discussion that has accumulated on an RFC's main view.
Reading is open by design. The framework's claim is that the *argument
behind a definition* is the evidence that the definition was earned,
and an argument that disappears behind a sign-in wall stops carrying
that evidence.
What you cannot do without an account: chat, propose a new RFC,
create a branch, open a PR, drop a flag, or post on a discussion
thread. Every write affordance is replaced with a sign-in prompt.
---
## Signing in
While the framework is in private beta, only invited email addresses
can complete sign-in. If your email is on the allowlist, the
"Sign in" button in the header completes the flow and lands you on
the catalog with full read and write access. If your email is not on
the allowlist, you'll be sent to a short "pending" page explaining
the gate.
Once you have an account, you're a **contributor** by default — the
role that grants every write affordance the app exposes, scoped by
the per-RFC and per-branch rules described below.
---
## Proposing a new RFC
A new RFC begins as a proposal. The "+ Propose new RFC" button at
the bottom of the catalog opens a small modal that collects four
things:
- **Title.** The word, concept, or topic this RFC would define.
- **Slug.** A kebab-cased identifier derived from the title. It is
the entry's stable handle from this moment until it graduates;
collisions with existing entries or open proposals are caught
inline.
- **Pitch.** One or two paragraphs answering *why this RFC is
needed*. This becomes the body of the entry.
- **Tags.** Optional. The AI suggests tags from the pitch; you can
accept, dismiss, or type your own.
Submitting the modal does one concrete thing: it opens a pull
request against the framework's meta repository, adding one new
file under `rfcs/`. There is no other Git artifact and no other
side-effect. You are returned to the **pending-idea view** for the
new proposal.
A pending idea is publicly readable but not yet a super-draft. The
catalog surfaces it in a "Pending ideas" disclosure at the bottom
of the list. A conversation can accumulate on the pending-idea view
before it is admitted — contributors can argue, in public, about
whether the entry belongs in the catalog at all.
Three outcomes are possible:
- **Merge.** An admin or owner merges the proposal PR. The entry
becomes a super-draft and graduates from the "Pending ideas"
section into the main catalog. Any conversation that accumulated
on the pending-idea view migrates with it.
- **Decline.** An admin or owner declines, attaching a written
comment. You see the comment on your next visit, along with a
one-click affordance to revise and re-propose.
- **Withdraw.** You can withdraw your own proposal at any time. The
entry will not appear in any default view; the conversation that
accumulated stays attached to the closed PR as historical record.
You are automatically the first owner of any RFC you propose. The
claim flow described under [Roles & permissions](#roles--permissions)
is for *other* contributors to add themselves as owners later, not
for the proposer.
---
## What a super-draft is
A super-draft is an entry that has been admitted to the catalog but
does not yet have its own dedicated repository. Most of the
argument that shapes a definition happens here. The framework
assumes — and the philosophy explicitly invites — that many
super-drafts will not survive the argument, and that is fine. The
entries that do survive earn their place in the catalog by being
defensible in public.
Opening a super-draft from the catalog gives you the same surface
an active RFC uses:
- The canonical body in the centre, read-only by default.
- A chat thread on the right where the public conversation lives.
- A breadcrumb dropdown listing any in-flight edit branches and
any open body-edit PRs against this entry.
- A "Start Contributing" affordance that cuts a fresh edit branch
and lands you in contribute mode.
Edits to a super-draft body propagate through pull requests against
the meta repository — there is no dedicated RFC repository yet.
---
## What an active RFC is
An active RFC is an entry that has been **graduated**. It has its
own dedicated repository, an integer `RFC-NNNN` identifier, and a
canonical body file (`RFC.md`) inside that repository. The catalog
distinguishes super-drafts and active RFCs at a glance.
Opening an active RFC gives you:
- `main` — the canonical body, always read-only. Changes to `main`
arrive exclusively through pull requests.
- A breadcrumb listing every open branch and pull request on this
RFC.
- A per-branch chat thread on the right. Each branch has its own
conversation, including `main` itself.
- A "Start Contributing" affordance: on `main` it cuts a new branch
and lands you on it in contribute mode; on any other branch you
already have push access to, it flips that branch into
contribute mode.
---
## Discussion vs contribution
The framework draws an explicit distinction between two surfaces
that other tools tend to conflate:
- **Discussion** is what the RFC is *for*. The chat thread on an
RFC's main view is the place for "what about this part?" or
"have we considered…?" questions that don't yet warrant proposing
a specific edit. Posting on a discussion thread does not create
any Git artifact; the conversation lives in the app database.
- **Contribution** is how an RFC *changes*. Editing the canonical
body requires opening a branch and, eventually, a pull request.
The pull request is the place a specific proposed change is
reviewed and merged.
Reading both surfaces is open to anonymous visitors. Posting on
either requires a contributor account.
---
## Working on a branch
Contribute mode flips one branch into edit-enabled. The centre
column splits: a markdown source pane on the left, a live-rendered
preview on the right. Fenced `mermaid` blocks render as diagrams in
the preview.
Two kinds of edits accumulate on a branch:
- **AI-proposed changes.** You ask the AI a question or request a
revision in the branch's chat. When the AI proposes a concrete
edit, that edit appears as a *change card* in a panel below the
chat — not yet applied to the document. You can **accept**,
**decline**, or **edit before accepting**. Accepting produces
one commit on the branch with the original text, the proposed
text, and the AI's reason recorded in the commit body.
- **Manual edits.** Typing directly into the source pane buffers
locally and flushes as a single commit on an idle window, a
branch switch, or an explicit "Save now" button. Manual edits
also appear as change cards in the same panel — same evidence
shape, different author.
Every accepted change is one commit. The framework does not
support squash-merges or fixup-style cleanups: the per-change
commit granularity is the framework's evidence unit, and
collapsing it would erase what was earned.
### Discuss mode vs contribute mode
A branch defaults to discuss mode — read-only, with chat enabled.
AI proposals still appear in chat, but they are *buffered* rather
than applied; a single CTA invites you to flip the branch into
contribute mode if you want to act on them. The toggle is an
*intent* affordance, not a permission one. If you don't have push
access to the branch, the toggle is disabled with a sign-in or
request-access path.
`main` is special: contribute mode is never available there. The
"Start Contributing" button on `main` always cuts a new branch.
### Flags
Anywhere you can read, you can drop a flag. A flag is the
lightweight "I'm pointing at this, it's a problem" gesture — a
single short declarative statement anchored to a passage. Creating
a flag requires a contributor account but does not require push
access to the branch: any signed-in contributor who can read a
passage can point at it and say it's wrong.
Flags don't block PR merges by design — making them a merge gate
would re-create the failure mode where contributors hastily "resolve"
threads to unblock a button. Flags are prominent on PR headers but
non-blocking.
### Branch visibility
A new branch is publicly readable by default. The branch creator
can flip a branch to private, in which case only the creator, any
explicit grantees, and the RFC's per-RFC owners and arbiters can
read it. Owners and arbiters can flip it back.
**Opening a PR makes the branch fully public.** If your branch is
currently private, the "Open PR" affordance asks you to confirm
this before submitting. There is no concept of a private PR — the
framework's evidence claim depends on the argument being readable.
### Who can push to a branch
Every branch has one of three contribute modes:
- **`just-me`** (default) — only the branch creator can push.
- **`specific`** — only the branch creator and explicitly granted
contributors can push.
- **`any-contributor`** — any signed-in contributor can push.
The branch creator and the RFC's per-RFC owners and arbiters can
change this setting at any time.
### Branch hygiene
A branch with no associated PR auto-closes after 30 days of
inactivity. A closed branch is deleted from the Git host 60 days
later. Closed branches remain in the catalog under a "show closed"
filter — closing is a state, not a censorship event. The chat
attached to a closed or deleted branch is preserved as historical
record.
Owners and arbiters can *pin* a branch to disable the auto-close
timer if the work is paused but legitimately ongoing.
---
## Opening and reviewing a pull request
A pull request is the deliberate "ready for review" gesture for
work that has accumulated on a branch. The "Open PR" affordance is
available on any branch with at least one commit ahead of `main`.
The PR creation modal collects two AI-drafted fields, both editable
before submit:
- **Title.** A one-line description of the change, in spec voice.
- **Description.** Two to four sentences pulling from the branch
chat, written for an arbiter.
There is no reviewer picker. The RFC's arbiters are the implicit
reviewer set.
### The PR review page
The review page shows the diff, the branch's compressed chat
(messages that produced accepted changes are expanded, the rest is
behind a "Show full conversation" toggle), and the review-comment
surface inline below the chat.
Review comments are not a separate concept from chat — they live in
the same thread, anchored to a range in the diff. The framework's
claim is that the disagreement an arbiter raises about a proposed
change is the same *kind* of thing as the disagreement that
produced the proposed change in the first place, and the two should
share a surface.
Each PR records a per-user seen-cursor. New diff hunks and new
conversation messages since your last visit render with a subtle
accent. The cursor advances on view; you do not have to mark
anything as read.
### Merging a PR
Per-RFC owners and arbiters can merge; app-wide admins and owners
also retain this capability. The merge produces a no-fast-forward
commit on `main`, preserving every per-acceptance commit as an
individually reachable node in `main`'s history.
Merge is hard-blocked **only** by Git-level conflicts with `main`.
Open review threads, pending change-cards, unresolved chat threads,
and open flags do not block merge by design.
### Conflicts with main
A conflict surfaces on the PR page as a read-only banner. A "Start
resolution branch" affordance cuts a fresh branch off `main`'s
current tip, replays the work into it (asking the AI to resolve
unambiguous conflicts, surfacing the rest for you), and opens a new
PR. The original PR auto-closes when the resolution PR merges.
Fixup commits on the existing branch are not supported. Per-change
commit granularity is the framework's evidence unit; admitting
"fix merge conflict with main" commits would dilute it.
---
## Graduation: super-draft → active RFC
Graduation is the moment a super-draft becomes a canonical entry
in the catalog. It is initiated by an app-wide admin, an app-wide
owner, or one of the RFC's per-RFC owners or arbiters from the
super-draft's page.
Two preconditions block the action:
- **The super-draft must have at least one owner.** The proposer
is automatically the first owner; if they have stepped away, any
contributor can use the "Claim ownership" affordance to add
themselves.
- **No open body-edit PRs against the super-draft's entry.** An
open body-edit PR would attempt to re-introduce a body to a
frontmatter-only entry after graduation runs. Merge or withdraw
them first.
When the dialog confirms, the framework runs a transactional
sequence: create a fresh Git repository for the RFC, seed it with
the super-draft's body as `RFC.md`, update the meta-repo entry to
`state: active` with the integer ID and the new repository's URL,
auto-merge that update. If any step fails partway, the sequence
rolls back — the half-created repository is deleted and the
unmerged update is abandoned. The dialog shows each step in flight
and tells you exactly what happened.
The chat thread on the super-draft moves to the new repository's
`main` chat at graduation. Edit-branch chats from the super-draft
phase stay attached to their original branches on the meta repo
and surface from the new RFC view under a "Pre-graduation history"
section.
Graduation is not reversible. The path forward from an active RFC
is withdrawal, not back to super-draft.
---
## Withdrawing and reopening
An active RFC or a super-draft can be withdrawn by the proposer
(for a super-draft they proposed) or by an admin or owner. A
withdrawn entry stays in the catalog as a historical record but is
hidden from default views. The entry is filterable back in.
An admin or owner can reopen a withdrawn entry back into the
super-draft state. The history is preserved across the transition.
---
## AI in the chat
The chat on every RFC, super-draft, branch, and PR has an AI
participant by default. The framework treats the AI as one voice
among many in a public argument — not an oracle, and not a
co-author whose name lands on commits.
You invoke the AI by writing into the chat composer and submitting.
Each message can pick a model from the picker (the option list is
configurable per RFC). The AI responds in the chat; when its
response includes a concrete change to the document, that change
appears as a card you can accept, decline, or edit.
When you accept an AI's proposed change, the commit's
`On-behalf-of:` trailer names *you*, not the AI. The AI's authorship
survives only as evidence — the original proposal in the commit body
and the message that produced it in the chat record. The framework
is explicit about this: AI participation produces evidence; it does
not produce authorship.
Two configuration knobs scope AI participation per RFC:
- **Which models are available.** The meta-repo entry's frontmatter
carries an optional `models:` list. Absent means the RFC inherits
whatever models the deployment is provisioned to run. An empty
list (`models: []`) opts the RFC out of AI entirely — every AI
surface is absent rather than disabled-but-present.
- **Whose credentials pay.** By default the deployment operator's
API credentials cover AI calls on every RFC. A `funder:`
frontmatter field can name a single contributor whose registered
credentials pay for AI calls on this RFC instead. The named
contributor must explicitly consent from their settings page;
either side can revoke at any time.
Per-RFC AI configuration is edited through the meta-repo PR flow
that governs the rest of the entry's frontmatter — by the RFC's
per-RFC owners and arbiters, or by app-wide admins or owners.
---
## Notifications
The framework's public-async work model produces signals that
shouldn't all reach you the same way. Five surfaces compose:
- **In-app inbox.** The durable triage surface. One mental space
across every RFC you have any relationship to, with per-RFC and
per-category filters. Reachable from the inbox icon in the
header.
- **Badges.** Ambient pull-ins. A single integer beside the inbox
icon (count of unread notifications). A small binary dot on
individual catalog rows for watched RFCs with unseen activity.
No per-row counts and no per-section counts.
- **Toasts.** Transient mid-session signals. Used only for your own
actions completing, and for events arriving on the view you're
currently looking at.
- **Email.** The single channel that escapes the app. Opt-in per
category, conservative defaults. One-click unsubscribe per
category.
- **Digest.** Aggregation for activity on watched RFCs you haven't
triaged through any other channel.
### Watch states
Every RFC has one of three implicit relationship states for you:
- **Watching.** You receive structural signals for the RFC.
- **Following.** You receive only churn-grade signals (new
commits, new chat messages on threads you didn't participate
in). This is a lighter relationship than watching.
- **Muted.** You receive no signals for the RFC. The mute is
per-RFC and self-imposed; it does not affect what others see
or what reaches you on *other* RFCs.
Watch states transition automatically based on your participation,
with explicit overrides available from each RFC's header and from
the notification settings page.
### Email categories
Four categories with distinct defaults:
- **Personal-direct events** — default on. Signals where you are
the named subject. The contract is that when your name is on the
action, the framework reaches out of band.
- **Watched-RFC structural events** — default off. PR opened on a
watched RFC, PR merged, graduation, withdrawal. Inbox and badges
carry these by default; the email toggle is opt-in.
- **Watched-RFC churn** — permanently off, by design. Per-commit
and per-message email is intentionally not offered. The digest
aggregates this activity weekly.
- **Admin-actionable events** — default on for admins and owners,
unused for contributors.
### Quiet hours
You can set a daily window during which email notifications are
held. Messages held during the window are released at window end —
bundled into a single "Activity while you were away" email if a
threshold accumulated, otherwise sent individually.
---
## Roles & permissions
Authorization in this framework is owned by the app itself, not by
the Git host. The Git host sees only a single bot account — every
commit, every PR, every merge passes through it on a user's behalf
— and the *app* decides which users are authorized to ask the bot
to do which things.
### The four app-wide roles
Each role is a strict superset of the one below it.
1. **Anonymous.** Anyone who has not signed in. Can read public
RFCs, public branches, and public PRs; cannot chat, propose,
create branches, or open PRs.
2. **Contributor.** The default role for any authenticated
account. Adds everything anonymous can do, plus: propose new
RFCs, create branches on any RFC repository, open PRs from
branches they have push access to, post on chat anywhere they
can read, claim ownership of unclaimed super-drafts.
3. **Admin.** Adds the ability to act on any RFC, anywhere in the
framework. Concretely: merge any PR on any RFC, graduate any
super-draft, set branch visibility on anyone's behalf, withdraw
or reopen any entry, write-mute or restore any contributor,
grant or revoke the **admin** role.
4. **Owner.** Adds two capabilities admin does not have: grant or
revoke the **owner** role itself, and disable an account
entirely. The framework names a single "owner zero" at
bootstrap.
The practical difference between admin and owner is narrow but
load-bearing: admin is the operational tier — it does the day-to-
day moderation and stewardship work; owner is the tier that
controls the admin tier. Disabling an account and creating other
owners are owner-only because they affect the framework's chain of
authority itself.
The app refuses to let the last owner demote themselves silently —
losing the last owner would leave nobody able to grant the role
back. Role changes are recorded in an append-only `permission_events`
log; an admin's own admin/users page shows the log of who promoted,
demoted, or muted whom.
### Per-RFC delegated authority
The four roles above are framework-wide. Within an individual RFC,
the meta-repo entry's frontmatter names two additional groups:
- **`owners:`** — contributors elevated for this RFC. They can
grant push access on any branch in the RFC, merge any PR on the
RFC, change branch visibility, and withdraw the RFC.
- **`arbiters:`** — contributors with merge authority for this RFC.
Functionally similar to per-RFC owners for merge decisions; the
distinction matters in some configuration paths.
Per-RFC owners and arbiters are **not** app-wide admins. Their
elevated powers are scoped strictly to the RFC named in the
frontmatter. This is what lets the framework distribute work
without putting one person on the hook for every action.
The proposer of an RFC is automatically the first per-RFC owner.
Additional per-RFC owners are added through a "Claim ownership"
PR against the meta repository; app-wide admins or owners merge
it.
### Per-branch contribute grants
Within an RFC, the branch creator and the RFC's per-RFC owners
and arbiters can grant push access to specific contributors on a
specific branch — `specific` contribute mode, described under
"Working on a branch."
### The write-mute
An app-wide admin or owner can **mute** a contributor. A muted
account retains read access and keeps its existing branches, but
cannot create new branches, open new PRs, propose new RFCs, or
post chat. This is a moderation tool, distinct from removing the
account; restoring is the reverse gesture.
The write-mute applies only to contributors. Promoting a user to
admin or owner is the way to remove a user's write-restriction in
the structural sense; the write-mute is for *retaining* an account
while removing its ability to act.
Every mute and every restore is recorded in `permission_events`.
### Three different "mutes"
The word "mute" appears in three structurally distinct places.
They share a word and nothing else.
- **Write-mute.** Admin-imposed. Removes a contributor's ability
to post or push. Described above.
- **Per-RFC notification mute.** Self-imposed. Sets your watch
state on a specific RFC to *muted* — you stop receiving signals
for that RFC, in inbox, badges, and email. Does not affect what
others see.
- **Per-user notification mute.** Self-imposed. Suppresses
notifications produced by a specific other user, anywhere in
the framework. Notification-volume only — it does not affect
what you can read.
A write-muted contributor continues to receive notifications
normally, so they can triage what they can't act on, and so a
restore lands cleanly.
### Audit trail
Every gesture that changes app state — role changes, mutes,
graduations, withdrawals, grant changes — is recorded in
append-only logs the app maintains. Git commit history is for
code archaeology; the app's audit log is the accountability
record. An admin's page surfaces both `permission_events` (the
role/mute log) and `actions` (the state-transition log) for
review.
---
## Where to learn more
- The framework's *why* lives in [the philosophy
document](/philosophy).
- The binding technical contract — section numbers (`§n.n`)
referenced throughout this guide — is in `SPEC.md` in the
framework's source repository.
- Deployment operators have their own recipe in
`docs/DEPLOYMENTS.md`.
+1 -1
View File
@@ -1 +1 @@
0.10.0
0.14.0
+12
View File
@@ -26,6 +26,7 @@ from . import (
api_prs,
auth,
db,
docs as docs_mod,
entry as entry_mod,
cache,
funder,
@@ -122,6 +123,17 @@ def make_router(
payload = philosophy.load()
return {"body": payload["body"]}
# ---------------------------------------------------------------
# /api/docs — DOCS.md served verbatim. Sibling of /api/philosophy:
# no auth gate, same disk-first load + cache shape, same intent —
# public read surface for a markdown file checked into the repo.
# ---------------------------------------------------------------
@router.get("/api/docs")
async def get_docs() -> dict[str, Any]:
payload = docs_mod.load()
return {"body": payload["body"]}
# ---------------------------------------------------------------
# Auth surface — reads role from our users table per §6.
# ---------------------------------------------------------------
+61
View File
@@ -0,0 +1,61 @@
"""User-facing docs source.
Mirrors `philosophy.py` shape. Serves `DOCS.md` from the repo root —
the framework's plain-prose user guide to roles, contribution flow,
and notification surfaces, distinct from the binding `SPEC.md`. Read
from disk on first call and cached in-process; the periodic
reconciler can call `refresh()` to pick up out-of-band edits.
`DOCS_PATH` overrides the default location if a deployment hosts the
file elsewhere (a meta-repo working-tree clone, a sync target, etc.).
"""
from __future__ import annotations
import logging
import os
import threading
from pathlib import Path
log = logging.getLogger(__name__)
_DEFAULT_PATH = Path(__file__).resolve().parents[2] / "DOCS.md"
_lock = threading.Lock()
_cache: dict | None = None
def _resolved_path() -> Path:
override = os.environ.get("DOCS_PATH", "").strip()
if override:
return Path(override).expanduser().resolve()
return _DEFAULT_PATH
def load(force: bool = False) -> dict:
"""Return the cached `{body, path, mtime}` payload, reading from disk
on first call or when `force=True`.
"""
global _cache
with _lock:
if _cache is not None and not force:
return _cache
path = _resolved_path()
try:
text = path.read_text(encoding="utf-8")
mtime = path.stat().st_mtime
except FileNotFoundError:
log.warning("DOCS.md not found at %s — serving placeholder", path)
text = (
"# DOCS.md not found\n\n"
"The deployment is missing its user guide. Set "
"DOCS_PATH or place DOCS.md at the project root."
)
mtime = 0.0
_cache = {"body": text, "path": str(path), "mtime": mtime}
return _cache
def refresh() -> dict:
"""Force-reread from disk. Returns the new payload."""
return load(force=True)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rfc-app-frontend",
"private": true,
"version": "0.10.0",
"version": "0.14.0",
"type": "module",
"scripts": {
"dev": "vite",
+13
View File
@@ -11,6 +11,7 @@ import Landing from './components/Landing.jsx'
import Login from './components/Login.jsx'
import BetaPending from './components/BetaPending.jsx'
import Philosophy from './components/Philosophy.jsx'
import Docs from './components/Docs.jsx'
import NotificationSettings from './components/NotificationSettings.jsx'
import Admin from './components/Admin.jsx'
import ToastHost, { showToast } from './components/ToastHost.jsx'
@@ -111,6 +112,9 @@ export default function App() {
<Link to="/philosophy" className="header-about" title="Why this exists (§14)">
About
</Link>
<Link to="/docs" className="header-about" title="User guide">
Docs
</Link>
{viewer && (
<Link to="/settings/notifications" className="header-settings" title="Notification settings (§15)">
Settings
@@ -153,6 +157,7 @@ export default function App() {
<Route path="/login" element={<Login />} />
<Route path="/beta-pending" element={<BetaPending viewer={viewer} />} />
<Route path="/philosophy" element={<PhilosophyWithSidebar viewer={viewer} />} />
<Route path="/docs" element={<DocsWithSidebar viewer={viewer} />} />
{/* §14.5 / §14.6: cookie-consent companions to /philosophy.
Available to anonymous and authenticated viewers alike. */}
<Route path="/privacy" element={<PolicyShell><Privacy /></PolicyShell>} />
@@ -221,6 +226,14 @@ function PhilosophyWithSidebar({ viewer }) {
)
}
function DocsWithSidebar({ viewer }) {
return (
<main className="chrome-pane">
<Docs authenticated={!!viewer} />
</main>
)
}
function NotificationSettingsWithSidebar({ viewer }) {
return (
<main className="chrome-pane">
+4
View File
@@ -632,6 +632,10 @@ export async function getPhilosophy() {
return jsonOrThrow(await fetch('/api/philosophy'))
}
export async function getDocs() {
return jsonOrThrow(await fetch('/api/docs'))
}
// ---------------------------------------------------------------------------
// Slice 7: admin neighborhood (§17 admin/* + user search for the §15.8 mute
// typeahead).
+51
View File
@@ -0,0 +1,51 @@
// `/docs` — the user-facing guide.
//
// Sibling of Philosophy.jsx: same chrome, same data path, different
// source file. Renders DOCS.md verbatim with light chrome around it.
// Reachable anonymously, same as `/philosophy`, so a visitor can read
// the guide before deciding to sign in.
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import MarkdownPreview from './MarkdownPreview.jsx'
import { getDocs } from '../api.js'
export default function Docs({ authenticated }) {
const [body, setBody] = useState('')
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
const navigate = useNavigate()
useEffect(() => {
let active = true
getDocs()
.then(r => { if (active) setBody(r.body || '') })
.catch(e => { if (active) setError(e.message || String(e)) })
.finally(() => { if (active) setLoading(false) })
return () => { active = false }
}, [])
return (
<div className="philosophy-page">
<header className="philosophy-header">
<button
className="philosophy-back"
onClick={() => (history.length > 1 ? navigate(-1) : navigate('/'))}
>
Back
</button>
<span className="philosophy-title">User guide</span>
{!authenticated && (
<Link className="philosophy-signin" to="/">Home</Link>
)}
</header>
<article className="philosophy-body">
{loading && <p className="muted">Loading</p>}
{error && <p className="error">Could not load the guide: {error}</p>}
{!loading && !error && (
<MarkdownPreview content={body} />
)}
</article>
</div>
)
}