§22 M3-backend Plan B (write path, propose): project-scoped propose (v0.38.0)

A new entry can be proposed into a specific project; it lands in that project's
content repo and shows under that project's proposals. A non-default project is
no longer read-only.

- api.py: POST /api/projects/{pid}/rfcs/propose (propose body extracted into a
  project-parameterized helper; unscoped /api/rfcs/propose kept as default
  compat). Slug uniqueness, idea-PR reservation, landing state, and the
  proposed_use_cases row scoped to the target project. GET
  /api/projects/{pid}/proposals.
- cache.py: refresh_meta_pulls loops every project's content_repo, stamping
  cached_prs.project_id; projects.content_repo(pid) helper.
- frontend: proposeRFC(projectId,…)/listProposals(projectId); ProposeModal
  takes projectId; App resolves current project from the /p/<id>/ URL; Catalog
  lists that project's proposals.
- tests: test_project_scoped_propose.py (lands scoped + gated 404). 447 backend
  + 11 Vitest green; clean build.

Known limitation: branch/PR/graduation edit flows + default-id re-stamp not yet
scoped (next slice). Per docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-04 07:02:19 -07:00
parent 455ef33b29
commit fec51bdbb6
11 changed files with 205 additions and 34 deletions
+7 -1
View File
@@ -61,6 +61,11 @@ export default function App() {
// §22.9 — runtime deployment config (name for the brand, default project id
// for building corpus links this slice; see DeploymentProvider).
const deployment = useDeployment()
// §22.4 — the project the viewer is currently in (from the /p/<id>/ URL),
// so the propose modal (App-level chrome, above the route tree) targets the
// right project. Falls back to the deployment default off a project route.
const _projMatch = location.pathname.match(/^\/p\/([^/]+)/)
const currentProjectId = (_projMatch && _projMatch[1]) || deployment.defaultProjectId
// #28 Parts 23: the LinkedText create/contribute affordances route via
// query params so they need no prop-threading from deep in a comment
// list. `?propose=<term>` opens the propose modal pre-filled;
@@ -372,12 +377,13 @@ export default function App() {
<ProposeModal
viewer={viewer}
initialTitle={proposeParam || ''}
projectId={currentProjectId}
onClose={() => { setProposeOpen(false); clearParams('propose') }}
onSubmitted={({ pr_number }) => {
setProposeOpen(false)
clearParams('propose')
setCatalogVersion(v => v + 1)
navigate(proposalPath(deployment.defaultProjectId, pr_number))
navigate(proposalPath(currentProjectId, pr_number))
}}
/>
)}