From de28272914204c80f4b4f37c1ecad5ad8a0cc1e2 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 28 May 2026 03:08:28 -0700 Subject: [PATCH] Release 0.14.0: public /docs user guide (DOCS.md served at /docs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 73 ++++ DOCS.md | 605 +++++++++++++++++++++++++++++++ VERSION | 2 +- backend/app/api.py | 12 + backend/app/docs.py | 61 ++++ frontend/package.json | 2 +- frontend/src/App.jsx | 13 + frontend/src/api.js | 4 + frontend/src/components/Docs.jsx | 51 +++ 9 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 DOCS.md create mode 100644 backend/app/docs.py create mode 100644 frontend/src/components/Docs.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index b13e426..c411273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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": "" }`. 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 diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..22fea51 --- /dev/null +++ b/DOCS.md @@ -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`. diff --git a/VERSION b/VERSION index 78bc1ab..a803cc2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.0 +0.14.0 diff --git a/backend/app/api.py b/backend/app/api.py index 732c9a1..8d8edcf 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -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. # --------------------------------------------------------------- diff --git a/backend/app/docs.py b/backend/app/docs.py new file mode 100644 index 0000000..63a209f --- /dev/null +++ b/backend/app/docs.py @@ -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) diff --git a/frontend/package.json b/frontend/package.json index ac2ab48..7d4027f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.10.0", + "version": "0.14.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 928380b..247f955 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() { About + + Docs + {viewer && ( Settings @@ -153,6 +157,7 @@ export default function App() { } /> } /> } /> + } /> {/* §14.5 / §14.6: cookie-consent companions to /philosophy. Available to anonymous and authenticated viewers alike. */} } /> @@ -221,6 +226,14 @@ function PhilosophyWithSidebar({ viewer }) { ) } +function DocsWithSidebar({ viewer }) { + return ( +
+ +
+ ) +} + function NotificationSettingsWithSidebar({ viewer }) { return (
diff --git a/frontend/src/api.js b/frontend/src/api.js index e63ef5a..76a4967 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -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). diff --git a/frontend/src/components/Docs.jsx b/frontend/src/components/Docs.jsx new file mode 100644 index 0000000..7af0c9d --- /dev/null +++ b/frontend/src/components/Docs.jsx @@ -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 ( +
+
+ + User guide + {!authenticated && ( + Home + )} +
+
+ {loading &&

Loading…

} + {error &&

Could not load the guide: {error}

} + {!loading && !error && ( + + )} +
+
+ ) +}