Slice 1: scaffolding + propose-to-super-draft vertical

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>
This commit is contained in:
Ben Stull
2026-05-24 04:31:11 -07:00
commit 779ba6db59
42 changed files with 10385 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
// api.js — every backend call lives here.
//
// All write requests pass {credentials: 'include'} implicitly because
// the dev proxy and the production deploy serve the API from the same
// origin as the frontend. If you split origins later, change here.
async function jsonOrThrow(res) {
if (!res.ok) {
let detail = ''
try {
const body = await res.json()
detail = body.detail || JSON.stringify(body)
} catch {
detail = await res.text()
}
const error = new Error(detail || `HTTP ${res.status}`)
error.status = res.status
throw error
}
return res.json()
}
export async function getMe() {
const res = await fetch('/api/auth/me')
return jsonOrThrow(res)
}
export async function listRFCs() {
return jsonOrThrow(await fetch('/api/rfcs'))
}
export async function getRFC(slug) {
return jsonOrThrow(await fetch(`/api/rfcs/${slug}`))
}
export async function listProposals() {
return jsonOrThrow(await fetch('/api/proposals'))
}
export async function getProposal(prNumber) {
return jsonOrThrow(await fetch(`/api/proposals/${prNumber}`))
}
export async function proposeRFC({ title, slug, pitch, tags }) {
const res = await fetch('/api/rfcs/propose', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, slug, pitch, tags: tags || [] }),
})
return jsonOrThrow(res)
}
export async function mergeProposal(prNumber) {
const res = await fetch(`/api/proposals/${prNumber}/merge`, { method: 'POST' })
return jsonOrThrow(res)
}
export async function declineProposal(prNumber, comment) {
const res = await fetch(`/api/proposals/${prNumber}/decline`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment }),
})
return jsonOrThrow(res)
}
export async function withdrawProposal(prNumber) {
const res = await fetch(`/api/proposals/${prNumber}/withdraw`, { method: 'POST' })
return jsonOrThrow(res)
}