revudocs
Reference

API & routes

The RevuApi methods and their HTTP routes — how the contract maps mechanically onto the revud daemon.

The RevuApi interface is the one boundary the UI talks through, and it maps mechanically onto HTTP: one route per method. The route table lives once, in the shared package (packages/shared/src/http.ts), and both revud (which serves the contract) and the fetch adapter (which consumes it) import it — so the wire mapping is defined exactly once and the two ends cannot diverge.

The methods

RevuApi (packages/shared/src/api/client.ts) is the full surface. Reads are the shared cost; writes are cheap — one call per review, regardless of how many comments it carries.

MethodWhat it does
getSession()The app's own session — identity injected at startup, not GitHub's.
listPulls({ etag? })The live PR list. The only surface safe to poll: a 304 is free against the shared rate bucket.
syncPull(prNumber, { signal? })One burst: pull the PR down whole and cache it. May resolve partial.
getSnapshot(prNumber)The cached snapshot, no network. null if the PR was never synced.
getBlob(sha)A content-addressed blob from the snapshot store. No network for a synced PR.
listReviewThreads(prNumber)Threads from the cached snapshot's mutable half.
replyToThread(prNumber, threadId, body)A single-comment reply — an immediate write, not drafted.
resolveThread(prNumber, threadId, resolved)The GraphQL resolve/unresolve mutation.
addReaction(prNumber, commentId, reaction)Add a reaction; returns the updated rollup.
submitReview(input)The atomic submit: one call carrying every pending comment. Returns head_moved as a value, never throws for it.
reconcileDraft(prNumber)Classify each pending comment against the fresh snapshot: clean / drifted / lost. A pure read.
getDraft / saveDraft / discardDraftThe per-human draft — broker-side, invisible to GitHub, survives a reload and a rebuild.
getFileViewed / setFileViewedPer-human, per-PR viewed state.
getPreferences / setPreferencesPer-human workspace preferences (not scoped to any PR). setPreferences merges a partial patch.
getRateLimit()Current shared-bucket status, for honest error copy.

The types & errors page has the exact shapes of the arguments and results.

The HTTP route table

Every method maps to exactly one route, keyed by method name in ROUTES (packages/shared/src/http.ts). Path segments prefixed with : are parameters: :n (pull number), :sha (blob SHA), :threadId, and :id (comment id). Bodies are JSON; casing follows the wire types unchanged.

RevuApi methodMethodPathBody
getSessionGET/api/session—
listPullsGET/api/pulls— (conditional; see below)
syncPullPOST/api/pulls/:n/sync—
getSnapshotGET/api/pulls/:n/snapshot—
getBlobGET/api/blobs/:sha—
listReviewThreadsGET/api/pulls/:n/threads—
replyToThreadPOST/api/pulls/:n/threads/:threadId/reply{ body }
resolveThreadPOST/api/pulls/:n/threads/:threadId/resolve{ resolved }
addReactionPOST/api/comments/:id/reactions{ reaction }
submitReviewPOST/api/pulls/:n/reviewSubmitReviewInput
reconcileDraftGET/api/pulls/:n/reconcile—
getDraftGET/api/pulls/:n/draft—
saveDraftPUT/api/pulls/:n/draftReviewDraft
discardDraftDELETE/api/pulls/:n/draft—
getFileViewedGET/api/pulls/:n/viewed—
setFileViewedPUT/api/pulls/:n/viewed{ path, viewed, blobSha }
getPreferencesGET/api/preferences—
setPreferencesPUT/api/preferencespartial HumanPreferences (e.g. { diffMode })
getRateLimitGET/api/rate-limit—

addReaction's route carries only the comment id, so the owning pull rides as a ?pr=<n> query param (or a prNumber field in the body) — the path contract is unchanged.

The one conditional route

listPulls is the only conditional surface, and the only genuinely live read in the app. The client sends If-None-Match; the server emits an ETag on the 200 response and replies 304 Not Modified when the header matches. A 304 costs nothing against the shared rate bucket, which is why the PR list is safe to poll while everything else is local. See the rate budget.

A 304 carries no body. On a 304 the client resolves a PullListResponse reconstructed from its last-known items with notModified: true and the echoed ETag. The mock has no real HTTP layer, so it returns a full body with notModified: true directly; the HTTP transport realizes the same PullListResponse via the 304 path. Either way the app sees one shape.

Non-error semantics that look like failures

Three responses look like errors but are not. Every implementation must preserve them verbatim, and the client must never treat them as error statuses:

  1. submitReview returns { status: 'head_moved', currentHeadSha, newCommits } as an HTTP 200 value, never an error status. The review bar routes into reconcile on it instead of failing.
  2. syncPull may resolve with Snapshot.partial non-null — a sync that died partway names what is missing. That is a 200 body, not an error.
  3. getSnapshot returns HTTP 200 with a JSON null body for a never-synced PR — never 404-as-error.

These are pinned by the conformance suite against every adapter. Full detail on the error envelope and codes is on the types & errors page.

What direct mode serves today

Direct mode's router serves the read-and-persist surface — getSession, syncPull, getSnapshot, getBlob, drafts, viewed, preferences, reconcileDraft — plus the full write path (submitReview, replyToThread, resolveThread, addReaction). Two routes are honest placeholders that answer 501 not_implemented in direct mode: listPulls (it has no poll loop) and getRateLimit, plus the GraphQL thread read listReviewThreads. These behave fully in mock mode. The dev-panel routes (GET/PUT /api/dev, POST /api/dev/reset) exist only in mock mode and are deliberately absent from the route table and refused in any other mode — identity must come from the channel, never from a client-settable value, and no unauthenticated route may wipe stored drafts.

On this page