You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
5.3 KiB
Python

"""Review Queue CLI: list, show, and resolve review items."""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
from libs.common.config import get_settings
from libs.common.logging import configure_logging, get_logger
from libs.db.session import get_session
from libs.review.queue import list_review_items, resolve_review_item
logger = get_logger(__name__)
def _fmt_item(item: object) -> str:
"""Format a ReviewItem for display."""
lines = [
f" review_id : {item.review_id}", # type: ignore[attr-defined]
f" entity_type : {item.entity_type}", # type: ignore[attr-defined]
f" entity_id : {item.entity_id}", # type: ignore[attr-defined]
f" priority : {item.priority}", # type: ignore[attr-defined]
f" status : {item.status}", # type: ignore[attr-defined]
f" reasons : {item.reason_codes}", # type: ignore[attr-defined]
f" created_at : {item.created_at_utc}", # type: ignore[attr-defined]
]
reviewer_id = getattr(item, "reviewer_id", None)
if reviewer_id:
lines.append(f" reviewer : {reviewer_id}")
if item.resolution_type: # type: ignore[attr-defined]
lines.append(f" resolution : {item.resolution_type}") # type: ignore[attr-defined]
lines.append(f" root_cause : {item.root_cause}") # type: ignore[attr-defined]
lines.append(f" notes : {item.notes}") # type: ignore[attr-defined]
return "\n".join(lines)
async def cmd_list(args: argparse.Namespace) -> None:
async with get_session() as session:
items = await list_review_items(
session,
status=args.status,
priority=getattr(args, "priority", None),
)
if not items:
print("No review items found.")
return
print(f"Found {len(items)} review item(s):\n")
for item in items:
print(f"[{item.priority}] {item.review_id}") # type: ignore[attr-defined]
print(f" entity: {item.entity_type}/{item.entity_id}") # type: ignore[attr-defined]
print(f" status: {item.status} | reasons: {item.reason_codes}") # type: ignore[attr-defined]
print()
async def cmd_show(args: argparse.Namespace) -> None:
from sqlalchemy import select
from libs.db.models import ReviewItem
async with get_session() as session:
import uuid
result = await session.execute(
select(ReviewItem).where(ReviewItem.review_id == uuid.UUID(args.review_id))
)
item = result.scalar_one_or_none()
if item is None:
print(f"Review item {args.review_id} not found.", file=sys.stderr)
sys.exit(1)
print(_fmt_item(item))
if item.snapshot_refs: # type: ignore[attr-defined]
print(f" snapshot_refs: {json.dumps(item.snapshot_refs, indent=4)}") # type: ignore[attr-defined]
if item.suggested_overrides: # type: ignore[attr-defined]
print(f" suggested_overrides: {json.dumps(item.suggested_overrides, indent=4)}") # type: ignore[attr-defined]
async def cmd_resolve(args: argparse.Namespace) -> None:
async with get_session() as session:
item = await resolve_review_item(
session=session,
review_id=args.review_id,
reviewer_id=args.reviewer,
resolution_type=args.resolution_type,
root_cause=args.root_cause or "",
notes=args.notes or "",
field_overrides=json.loads(args.overrides) if args.overrides else None,
)
await session.commit()
print(f"Resolved review item {item.review_id} as '{item.resolution_type}'.") # type: ignore[attr-defined]
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="ACE-F Review Queue CLI")
sub = p.add_subparsers(dest="command", required=True)
# list
p_list = sub.add_parser("list", help="List review items")
p_list.add_argument("--status", default="open", help="Filter by status (default: open)")
p_list.add_argument("--priority", default=None, help="Filter by priority (P0/P1/P2)")
# show
p_show = sub.add_parser("show", help="Show review item details")
p_show.add_argument("review_id", help="UUID of the review item")
# resolve
p_resolve = sub.add_parser("resolve", help="Resolve a review item")
p_resolve.add_argument("review_id", help="UUID of the review item")
p_resolve.add_argument("--reviewer", required=True, help="Reviewer ID/name")
p_resolve.add_argument(
"--resolution-type", required=True,
choices=["accepted", "corrected", "wont_fix", "escalated"],
)
p_resolve.add_argument("--root-cause", default="", help="Root cause description")
p_resolve.add_argument("--notes", default="", help="Free-text notes")
p_resolve.add_argument("--overrides", default=None, help="JSON string of field overrides")
return p
def main() -> None:
settings = get_settings()
configure_logging(settings.log_level)
parser = build_parser()
args = parser.parse_args()
handlers = {
"list": cmd_list,
"show": cmd_show,
"resolve": cmd_resolve,
}
handler = handlers.get(args.command)
if handler is None:
parser.print_help()
sys.exit(1)
asyncio.run(handler(args))
if __name__ == "__main__":
main()