Slice 8 WIP: §12 hygiene + §10.7 + routing + rollback cleanup

- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
  DigestScheduler shape; wires next to digest in main.py with the
  same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
  per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
  api_prs.py to {branch:path}; move the bare GET to the bottom of
  the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
  so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
  after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
  slashed-branch read; integration tests for the hygiene vertical
  cover the 30d close, 90d delete, post-merge delete, pinned
  exemption, per-user cursor preservation, no-notification rule,
  and the graduation-rollback cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-25 04:03:09 -07:00
parent 060fa408a2
commit 1a0c4428af
9 changed files with 965 additions and 105 deletions
+8 -3
View File
@@ -77,7 +77,7 @@ def make_router(
# produces the open-pr call; the draft is just a starting point.
# -------------------------------------------------------------------
@router.post("/api/rfcs/{slug}/branches/{branch}/pr-draft")
@router.post("/api/rfcs/{slug}/branches/{branch:path}/pr-draft")
async def draft_pr_text(slug: str, branch: str, request: Request) -> dict[str, Any]:
viewer = auth.require_contributor(request)
rfc = _require_active_rfc(slug)
@@ -107,7 +107,7 @@ def make_router(
# branch's read_public unconditionally.
# -------------------------------------------------------------------
@router.post("/api/rfcs/{slug}/branches/{branch}/open-pr")
@router.post("/api/rfcs/{slug}/branches/{branch:path}/open-pr")
async def open_pr(slug: str, branch: str, body: OpenPRBody, request: Request) -> dict[str, Any]:
viewer = auth.require_contributor(request)
rfc = _require_active_rfc(slug)
@@ -660,11 +660,16 @@ def make_router(
# exposed through this surface — the merge path is the only
# affordance an admin needs, and the §10 review machinery
# gracefully degrades for frontmatter-only PRs.
# Slice 8: §9.5 metadata-pane PRs (`meta_metadata`) land here
# too per the §19.2 "in-app merge for metadata PRs" candidate.
# The diff-rendered review surface degrades gracefully — there
# is no body diff worth reviewing — but the merge gesture lands
# in-app rather than forcing the Gitea round-trip.
row = db.conn().execute(
"""
SELECT * FROM cached_prs
WHERE rfc_slug = ? AND pr_number = ?
AND pr_kind IN ('rfc_branch', 'meta_body_edit', 'meta_claim')
AND pr_kind IN ('rfc_branch', 'meta_body_edit', 'meta_claim', 'meta_metadata')
""",
(slug, pr_number),
).fetchone()