"""Query helpers (part A) for AI PMO cadence service.""" from __future__ import annotations from .shared import ( Any, Optional, ProjectSignalLink, Signal, Task, WorkItem, and_, func, or_, select, utc_now_naive, ) class CadenceQueriesAMixin: """Work item/task/signal query helpers.""" async def _list_recent_work_items(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(WorkItem).where( and_( WorkItem.tenant_id == self.tenant_id, WorkItem.created_at >= since, ) ) if project_id is not None: stmt = stmt.where(WorkItem.project_id == int(project_id)) rows = (await self.db.execute(stmt.order_by(WorkItem.created_at.desc()).limit(50))).scalars().all() return [ { "id": int(row.id), "state": row.state, "intent": row.intent, "project_id": row.project_id, "signal_id": row.signal_id, "created_at": row.created_at.isoformat() if row.created_at else None, } for row in rows ] async def _count_work_items(self, *, state: str, project_id: Optional[int]) -> int: stmt = select(func.count(WorkItem.id)).where( and_( WorkItem.tenant_id == self.tenant_id, WorkItem.state == state, ) ) if project_id is not None: stmt = stmt.where(WorkItem.project_id == int(project_id)) return int((await self.db.execute(stmt)).scalar_one() or 0) async def _list_blocked_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(Task).where( and_( Task.tenant_id == self.tenant_id, Task.status == "blocked", ) ) if project_id is not None: stmt = stmt.where(Task.project_id == int(project_id)) rows = (await self.db.execute(stmt.order_by(Task.updated_at.desc()).limit(50))).scalars().all() return [ { "id": int(row.id), "project_id": int(row.project_id), "title": str(row.title or ""), "priority": str(row.priority or ""), } for row in rows ] async def _list_overdue_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]: now = utc_now_naive() stmt = select(Task).where( and_( Task.tenant_id == self.tenant_id, Task.due_date.is_not(None), Task.due_date < now, Task.status.not_in(["done", "completed", "closed"]), ) ) if project_id is not None: stmt = stmt.where(Task.project_id == int(project_id)) rows = (await self.db.execute(stmt.order_by(Task.due_date.asc()).limit(50))).scalars().all() return [ { "id": int(row.id), "project_id": int(row.project_id), "title": str(row.title or ""), "due_date": row.due_date.isoformat() if row.due_date else None, } for row in rows ] async def _list_recent_signals(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]: stmt = select(Signal).where( and_( Signal.tenant_id == self.tenant_id, Signal.created_at >= since, ) ) if project_id is not None: stmt = stmt.where( or_( Signal.matched_project_id == int(project_id), Signal.id.in_( select(ProjectSignalLink.signal_id).where( and_( ProjectSignalLink.tenant_id == self.tenant_id, ProjectSignalLink.project_id == int(project_id), ) ) ), ) ) rows = (await self.db.execute(stmt.order_by(Signal.created_at.desc()).limit(50))).scalars().all() return [ { "id": int(row.id), "title": str(row.title or ""), "status": str(row.status or ""), "signal_type": str(row.signal_type or ""), } for row in rows ]