feat(review): mark-reviewed action + §7 catalog unreviewed filter (§22.4c)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-03 23:56:58 -07:00
parent 2fe2a719ac
commit 76207bbb62
3 changed files with 152 additions and 3 deletions
+44 -3
View File
@@ -611,13 +611,16 @@ def make_router(
# --------------------------------------------------------------- # ---------------------------------------------------------------
@router.get("/api/rfcs") @router.get("/api/rfcs")
async def list_rfcs(request: Request) -> dict[str, Any]: async def list_rfcs(request: Request, unreviewed: str | None = None) -> dict[str, Any]:
"""§7's left pane data. """§7's left pane data.
The chip-filter / sort / search combinatorics live on the The chip-filter / sort / search combinatorics live on the
client — the server returns the full set and lets the chips client — the server returns the full set and lets the chips
narrow it. The set is small (hundreds, not thousands) for the narrow it. The set is small (hundreds, not thousands) for the
foreseeable future, so paginating here would buy nothing. foreseeable future, so paginating here would buy nothing.
§22.4c: pass `?unreviewed=true` to narrow to active entries
still awaiting owner review.
""" """
viewer = auth.current_user(request) viewer = auth.current_user(request)
viewer_id = viewer.user_id if viewer else None viewer_id = viewer.user_id if viewer else None
@@ -627,6 +630,10 @@ def make_router(
if not visible: if not visible:
return {"items": []} return {"items": []}
placeholders = ",".join("?" for _ in visible) placeholders = ",".join("?" for _ in visible)
params = list(visible)
unreviewed_clause = ""
if unreviewed is not None and unreviewed.lower() in ("1", "true", "yes"):
unreviewed_clause = " AND unreviewed = 1 AND state = 'active'"
rows = db.conn().execute( rows = db.conn().execute(
f""" f"""
SELECT slug, title, state, rfc_id, repo, SELECT slug, title, state, rfc_id, repo,
@@ -634,10 +641,10 @@ def make_router(
last_main_commit_at, last_entry_commit_at, updated_at last_main_commit_at, last_entry_commit_at, updated_at
FROM cached_rfcs FROM cached_rfcs
WHERE state IN ('super-draft', 'active') WHERE state IN ('super-draft', 'active')
AND project_id IN ({placeholders}) AND project_id IN ({placeholders}){unreviewed_clause}
ORDER BY COALESCE(last_main_commit_at, last_entry_commit_at) DESC ORDER BY COALESCE(last_main_commit_at, last_entry_commit_at) DESC
""", """,
visible, params,
).fetchall() ).fetchall()
starred = set() starred = set()
@@ -700,6 +707,40 @@ def make_router(
payload["proposed_use_case"] = uc["use_case"] if uc else None payload["proposed_use_case"] = uc["use_case"] if uc else None
return payload return payload
# ---------------------------------------------------------------
# §22.4c: mark-reviewed — clear an active entry's `unreviewed` flag
# ---------------------------------------------------------------
@router.post("/api/projects/{project_id}/rfcs/{slug}/mark-reviewed")
async def mark_reviewed(project_id: str, slug: str, request: Request) -> dict[str, Any]:
"""§22.4c — clear an active entry's `unreviewed` flag. Authority is the
§22.7 project superuser (project_admin or deployment owner/admin)."""
viewer = auth.require_user(request)
auth.require_project_readable(viewer, project_id)
if not auth.is_project_superuser(viewer, project_id):
raise HTTPException(403, "Only a project owner/admin can mark an entry reviewed")
row = db.conn().execute(
"SELECT state, unreviewed FROM cached_rfcs WHERE slug = ? AND project_id = ?",
(slug, project_id),
).fetchone()
if row is None:
raise HTTPException(404, "Not found")
if row["state"] != "active" or not row["unreviewed"]:
raise HTTPException(409, "Entry is not an unreviewed active entry")
try:
await bot.mark_entry_reviewed(
viewer.as_actor(),
org=config.gitea_org,
meta_repo=projects_mod.default_content_repo(config),
slug=slug,
reviewed_by=viewer.gitea_login,
reviewed_at=entry_mod.today(),
)
except GiteaError as e:
raise HTTPException(502, f"Gitea: {e.detail}")
await cache.refresh_meta_repo(config, gitea)
return {"ok": True}
# --------------------------------------------------------------- # ---------------------------------------------------------------
# §7.3 / §9.3: pending ideas # §7.3 / §9.3: pending ideas
# --------------------------------------------------------------- # ---------------------------------------------------------------
+50
View File
@@ -1069,6 +1069,56 @@ class Bot:
) )
return pr return pr
# ----- §22.4c: mark-reviewed (direct main write) -----
async def mark_entry_reviewed(
self,
actor: Actor,
*,
org: str,
meta_repo: str,
slug: str,
reviewed_by: str,
reviewed_at: str,
) -> None:
"""Clear §22.4c unreviewed on an active entry by rewriting its
frontmatter on main. Stamps the commit with the §6.5 On-behalf-of
trailer and writes an actions-log row, mirroring the graduation
stamp's bot-write shape."""
from . import entry as entry_mod
path = f"rfcs/{slug}.md"
result = await self._gitea.read_file(org, meta_repo, path, ref="main")
if result is None:
raise GiteaError(404, f"{path} not found")
text, sha = result
e = entry_mod.parse(text)
e.unreviewed = False
e.reviewed_at = reviewed_at
e.reviewed_by = reviewed_by
commit_message = _stamp_single(f"Mark {slug} reviewed", actor)
result2 = await self._gitea.update_file(
org, meta_repo, path,
content=entry_mod.serialize(e),
sha=sha,
message=commit_message,
branch="main",
author_name=actor.display_name,
author_email=actor.email or f"{actor.gitea_login}@users.noreply",
)
commit_sha = (
result2.get("commit", {}).get("sha")
or result2.get("content", {}).get("sha")
or ""
)
_log(
actor,
"mark_reviewed",
rfc_slug=slug,
bot_commit_sha=commit_sha,
details={"reviewed_by": reviewed_by, "reviewed_at": reviewed_at},
)
# ----- Per-RFC repo: seeding (test/dev fixtures, future graduation) ----- # ----- Per-RFC repo: seeding (test/dev fixtures, future graduation) -----
async def ensure_rfc_repo_seed( async def ensure_rfc_repo_seed(
+58
View File
@@ -0,0 +1,58 @@
"""§22.4c — owner/admin mark-reviewed clears the flag; catalog unreviewed filter."""
from __future__ import annotations
from fastapi.testclient import TestClient
from test_propose_vertical import ( # noqa: F401
app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as,
)
def _seed_unreviewed_active(fake, slug="feat"):
"""Put an active+unreviewed entry on the meta repo main + cache."""
from app import cache, entry as entry_mod
body = entry_mod.serialize(entry_mod.Entry(
slug=slug, title="Feat", state="active", unreviewed=True,
owners=["ben"], proposed_by="ben",
))
fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {"content": body, "sha": "s1"}
cache._upsert_cached_rfc(entry_mod.parse(body), body_sha="s1")
def test_catalog_unreviewed_filter(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_unreviewed_active(fake, "feat")
from app import cache, entry as entry_mod
ok = entry_mod.serialize(entry_mod.Entry(slug="ok", title="OK", state="active", owners=["ben"]))
fake.files[("wiggleverse", "meta", "main", "rfcs/ok.md")] = {"content": ok, "sha": "s2"}
cache._upsert_cached_rfc(entry_mod.parse(ok), body_sha="s2")
r = client.get("/api/rfcs", params={"unreviewed": "true"})
slugs = {i["slug"] for i in r.json()["items"]}
assert slugs == {"feat"}
def test_mark_reviewed_clears_flag(app_with_fake_gitea):
from app import db
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_unreviewed_active(fake, "feat")
provision_user_row(user_id=1, login="ben", role="owner")
sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")
r = client.post("/api/projects/default/rfcs/feat/mark-reviewed")
assert r.status_code == 200
row = db.conn().execute(
"SELECT unreviewed, reviewed_by FROM cached_rfcs WHERE slug='feat'"
).fetchone()
assert row["unreviewed"] == 0
assert row["reviewed_by"] == "ben"
def test_mark_reviewed_forbidden_for_non_superuser(app_with_fake_gitea):
app, fake = app_with_fake_gitea
with TestClient(app) as client:
_seed_unreviewed_active(fake, "feat")
provision_user_row(user_id=2, login="carol", role="contributor")
sign_in_as(client, user_id=2, gitea_login="carol", display_name="Carol", role="contributor")
r = client.post("/api/projects/default/rfcs/feat/mark-reviewed")
assert r.status_code == 403