Slice 4: super-draft body editing per §9.5 + §9.6
The §17 routing-collapse rule lands in api_branches.py and api_prs.py — every branches/<branch>/... and prs/<n>/... route dispatches on the entry's state to pick the right Gitea repo, and the body extracted from the entry's frontmatter envelope is what the editor and the diff see. The bot grows open_metadata_pr; cache grows refresh_meta_branches. Two §17 routes added: start-edit-branch and metadata. The §9.4 super-draft view replaces RFCView.jsx's Slice 2 placeholder; a metadata pane modal opens from the breadcrumb. Branch naming uses edit-<slug>-<6hex> to dodge the §19.2 path-routing candidate while preserving §9.5's structural shape. Covered by tests/test_super_draft_vertical.py (10 tests). The full Slices 1-4 suite is 35/35 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+148
-6
@@ -286,6 +286,112 @@ async def refresh_rfc_repo(config: Config, gitea: Gitea, slug: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def refresh_meta_branches(config: Config, gitea: Gitea) -> None:
|
||||
"""Mirror the meta repo's branches into `cached_branches` for super-draft
|
||||
edit branches, plus a per-slug `main` row that records the meta-repo
|
||||
main's tip sha so the §10.1 has-commits-ahead check works uniformly
|
||||
across active and super-draft surfaces.
|
||||
|
||||
Per the §5 super-draft scoping note, super-draft edits are branches on
|
||||
the meta repo. The naming Slice 4 picked is `edit-<slug>-<6hex>` —
|
||||
structurally `edit/<slug>/<auto-name>` per §9.5, with dashes in place
|
||||
of slashes per the §19.2 path-routing candidate.
|
||||
"""
|
||||
org, repo = config.gitea_org, config.meta_repo
|
||||
try:
|
||||
branches = await gitea.list_branches(org, repo)
|
||||
except GiteaError as e:
|
||||
log.warning("refresh_meta_branches: %s", e)
|
||||
return
|
||||
|
||||
meta_main_sha = ""
|
||||
meta_main_ts = None
|
||||
edit_keys_seen: set[tuple[str, str]] = set()
|
||||
for b in branches:
|
||||
name = b.get("name") or ""
|
||||
head_sha = (b.get("commit") or {}).get("id") or ""
|
||||
last_commit_at = (b.get("commit") or {}).get("timestamp")
|
||||
if name == "main":
|
||||
meta_main_sha = head_sha
|
||||
meta_main_ts = last_commit_at
|
||||
continue
|
||||
slug = _slug_from_branch_name(name)
|
||||
if not slug:
|
||||
continue
|
||||
rfc = db.conn().execute(
|
||||
"SELECT state FROM cached_rfcs WHERE slug = ?", (slug,)
|
||||
).fetchone()
|
||||
if not rfc or rfc["state"] != "super-draft":
|
||||
continue
|
||||
edit_keys_seen.add((slug, name))
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT INTO cached_branches (rfc_slug, branch_name, head_sha, state, last_commit_at)
|
||||
VALUES (?, ?, ?, 'open', ?)
|
||||
ON CONFLICT(rfc_slug, branch_name) DO UPDATE SET
|
||||
head_sha = excluded.head_sha,
|
||||
state = CASE WHEN cached_branches.state = 'closed' THEN 'closed' ELSE 'open' END,
|
||||
last_commit_at = excluded.last_commit_at
|
||||
""",
|
||||
(slug, name, head_sha, last_commit_at),
|
||||
)
|
||||
|
||||
# Synthesize a per-slug `main` row for every super-draft entry, so the
|
||||
# §10.1 has-commits-ahead check in api_prs.py works uniformly. The
|
||||
# head_sha is the meta-repo main's tip — every super-draft edit branch
|
||||
# diverges from this single point.
|
||||
if meta_main_sha:
|
||||
super_drafts = db.conn().execute(
|
||||
"SELECT slug FROM cached_rfcs WHERE state = 'super-draft'"
|
||||
).fetchall()
|
||||
for r in super_drafts:
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT INTO cached_branches (rfc_slug, branch_name, head_sha, state, last_commit_at)
|
||||
VALUES (?, 'main', ?, 'open', ?)
|
||||
ON CONFLICT(rfc_slug, branch_name) DO UPDATE SET
|
||||
head_sha = excluded.head_sha,
|
||||
last_commit_at = excluded.last_commit_at
|
||||
""",
|
||||
(r["slug"], meta_main_sha, meta_main_ts),
|
||||
)
|
||||
|
||||
# Mark previously-known edit branches that disappeared as deleted per
|
||||
# §11.5 / §12. Keep the row so chat history survives the branch's
|
||||
# deletion in Gitea.
|
||||
known = db.conn().execute(
|
||||
"""
|
||||
SELECT b.rfc_slug, b.branch_name
|
||||
FROM cached_branches b
|
||||
JOIN cached_rfcs r ON r.slug = b.rfc_slug
|
||||
WHERE r.state = 'super-draft'
|
||||
AND b.state != 'deleted'
|
||||
AND b.branch_name != 'main'
|
||||
"""
|
||||
).fetchall()
|
||||
for k in known:
|
||||
if (k["rfc_slug"], k["branch_name"]) not in edit_keys_seen:
|
||||
db.conn().execute(
|
||||
"UPDATE cached_branches SET state = 'deleted' WHERE rfc_slug = ? AND branch_name = ?",
|
||||
(k["rfc_slug"], k["branch_name"]),
|
||||
)
|
||||
|
||||
|
||||
def _slug_from_branch_name(name: str) -> str | None:
|
||||
"""Mirror of `_slug_from_head_branch` for branch-only inputs (no PR
|
||||
body to consult)."""
|
||||
if name.startswith("edit-"):
|
||||
body = name[len("edit-") :]
|
||||
if "-" in body:
|
||||
slug, _hex = body.rsplit("-", 1)
|
||||
return slug or None
|
||||
if name.startswith("edit/"):
|
||||
parts = name.split("/", 2)
|
||||
if len(parts) >= 2:
|
||||
return parts[1]
|
||||
return None
|
||||
|
||||
|
||||
async def refresh_meta_pulls(config: Config, gitea: Gitea) -> None:
|
||||
"""Reconcile open meta-repo PRs into cached_prs.
|
||||
|
||||
@@ -328,13 +434,28 @@ async def refresh_meta_pulls(config: Config, gitea: Gitea) -> None:
|
||||
pull["number"],
|
||||
pull.get("body") or "",
|
||||
)
|
||||
# §10.8 / Slice 4: a closed body-edit PR may have been withdrawn
|
||||
# by the contributor. Distinguish from a generic Gitea close via
|
||||
# the audit log — same shape api_prs.py uses for rfc_branch PRs.
|
||||
if state == "closed" and pr_kind == "meta_body_edit":
|
||||
withdrew = db.conn().execute(
|
||||
"""
|
||||
SELECT 1 FROM actions
|
||||
WHERE action_kind = 'withdraw_branch_pr'
|
||||
AND rfc_slug = ? AND pr_number = ? LIMIT 1
|
||||
""",
|
||||
(slug, pull["number"]),
|
||||
).fetchone()
|
||||
if withdrew:
|
||||
state = "withdrawn"
|
||||
merge_commit_sha = pull.get("merge_commit_sha")
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT INTO cached_prs
|
||||
(rfc_slug, pr_kind, repo, pr_number, title, description, state,
|
||||
opened_by, opened_at, merged_at, closed_at,
|
||||
head_branch, base_branch, head_sha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
head_branch, base_branch, head_sha, merge_commit_sha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(repo, pr_number) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
description = excluded.description,
|
||||
@@ -342,7 +463,8 @@ async def refresh_meta_pulls(config: Config, gitea: Gitea) -> None:
|
||||
opened_by = excluded.opened_by,
|
||||
merged_at = excluded.merged_at,
|
||||
closed_at = excluded.closed_at,
|
||||
head_sha = excluded.head_sha
|
||||
head_sha = excluded.head_sha,
|
||||
merge_commit_sha = COALESCE(excluded.merge_commit_sha, cached_prs.merge_commit_sha)
|
||||
""",
|
||||
(
|
||||
slug,
|
||||
@@ -359,6 +481,7 @@ async def refresh_meta_pulls(config: Config, gitea: Gitea) -> None:
|
||||
head_branch,
|
||||
(pull.get("base") or {}).get("ref") or "main",
|
||||
(pull.get("head") or {}).get("sha"),
|
||||
merge_commit_sha,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -374,7 +497,7 @@ def _resolve_actor(gitea_opener: str, bot_login: str, slug: str, pr_number: int,
|
||||
row = db.conn().execute(
|
||||
"""
|
||||
SELECT on_behalf_of FROM actions
|
||||
WHERE action_kind IN ('propose_rfc', 'open_body_edit_pr', 'open_claim_pr', 'open_metadata_pr')
|
||||
WHERE action_kind IN ('propose_rfc', 'open_body_edit_pr', 'open_branch_pr', 'open_claim_pr', 'open_metadata_pr')
|
||||
AND rfc_slug = ? AND pr_number = ?
|
||||
ORDER BY id LIMIT 1
|
||||
""",
|
||||
@@ -400,21 +523,39 @@ def _slug_from_head_branch(head_branch: str) -> str | None:
|
||||
parts = head_branch.split("/", 2)
|
||||
if len(parts) >= 2:
|
||||
return parts[1]
|
||||
if head_branch.startswith("edit-"):
|
||||
# §9.5 names the structural shape `edit/<slug>/<auto-name>`, but
|
||||
# FastAPI's default {branch} path-segment matcher refuses slashes
|
||||
# (the §19.2 routing candidate). Slice 4 picks the same dash-
|
||||
# separated workaround Slice 2 used for promote-to-branch:
|
||||
# `edit-<slug>-<6hex>`. The slug is the middle; the final
|
||||
# dash-segment is a 6-hex suffix.
|
||||
body = head_branch[len("edit-") :]
|
||||
if "-" in body:
|
||||
slug, _hex = body.rsplit("-", 1)
|
||||
return slug or None
|
||||
if head_branch.startswith("claim/"):
|
||||
return head_branch[len("claim/") :]
|
||||
if head_branch.startswith("metadata/"):
|
||||
return head_branch[len("metadata/") :]
|
||||
if head_branch.startswith("metadata-"):
|
||||
# §9.5 metadata-pane PRs use the same dash-separated branch shape
|
||||
# as edit branches, for the same routing reason.
|
||||
body = head_branch[len("metadata-") :]
|
||||
if "-" in body:
|
||||
slug, _hex = body.rsplit("-", 1)
|
||||
return slug or None
|
||||
return None
|
||||
|
||||
|
||||
def _kind_from_branch(head_branch: str) -> str:
|
||||
if head_branch.startswith("propose/"):
|
||||
return "idea"
|
||||
if head_branch.startswith("edit/"):
|
||||
if head_branch.startswith("edit/") or head_branch.startswith("edit-"):
|
||||
return "meta_body_edit"
|
||||
if head_branch.startswith("claim/"):
|
||||
return "meta_claim"
|
||||
if head_branch.startswith("metadata/"):
|
||||
if head_branch.startswith("metadata/") or head_branch.startswith("metadata-"):
|
||||
return "meta_metadata"
|
||||
return "idea" # fallback
|
||||
|
||||
@@ -475,6 +616,7 @@ class Reconciler:
|
||||
log.info("reconciler: starting sweep")
|
||||
try:
|
||||
await refresh_meta_repo(self._config, self._gitea)
|
||||
await refresh_meta_branches(self._config, self._gitea)
|
||||
await refresh_meta_pulls(self._config, self._gitea)
|
||||
# Per-RFC repos: refresh each active entry. Meta-repo refresh
|
||||
# must come first so newly-graduated entries land in
|
||||
|
||||
Reference in New Issue
Block a user