"""Shared loader for richer analysis project snapshots.""" from __future__ import annotations from typing import Any from sqlalchemy import and_, select from app.models.project import Project from app.models.signal import Signal from app.models.task import Task def _normalize_text(value: Any, *, max_len: int = 240) -> str: text = " ".join(str(value or "").split()).strip() if not text: return "" return text[:max_len] def _object_ref(*, kind: str, row: dict[str, Any] | None) -> dict[str, Any] | None: payload = dict(row or {}) title = _normalize_text(payload.get("title"), max_len=120) if not title: return None ref: dict[str, Any] = { "kind": kind, "title": title, } try: object_id = int(payload.get("id")) if payload.get("id") is not None else None except Exception: object_id = None if object_id is not None: ref["id"] = object_id return ref def _build_analysis_object_refs( *, tasks: list[dict[str, str]], signals: list[dict[str, str]], ) -> dict[str, Any]: _ = (tasks, signals) # Fail closed: without explicit structural task-signal relations in the data model, # do not synthesize business object refs from task/signal wording. return {} async def load_analysis_project_snapshot( *, db: Any, tenant_id: int | None, project_id: int | None, task_limit: int = 4, signal_limit: int = 6, ) -> dict[str, Any]: if project_id is None or db is None or not hasattr(db, "execute"): return {} project_filters = [Project.id == int(project_id)] task_filters = [Task.project_id == int(project_id)] signal_filters = [Signal.matched_project_id == int(project_id)] if tenant_id is not None: project_filters.append(Project.tenant_id == int(tenant_id)) task_filters.append(Task.tenant_id == int(tenant_id)) signal_filters.append(Signal.tenant_id == int(tenant_id)) project_row = ( await db.execute( select(Project).where(and_(*project_filters)).limit(1) ) ).scalar_one_or_none() if project_row is None: return {} task_rows = ( await db.execute( select(Task) .where(and_(*task_filters)) .order_by(Task.id.desc()) .limit(int(task_limit)) ) ).scalars().all() signal_rows = ( await db.execute( select(Signal) .where(and_(*signal_filters)) .order_by(Signal.id.desc()) .limit(int(signal_limit)) ) ).scalars().all() tasks = [ { "id": getattr(row, "id", None), "title": _normalize_text(getattr(row, "title", ""), max_len=120), "description": _normalize_text(getattr(row, "description", ""), max_len=220), "status": _normalize_text(getattr(row, "status", ""), max_len=40), } for row in task_rows if _normalize_text(getattr(row, "title", ""), max_len=120) ] signals = [ { "id": getattr(row, "id", None), "title": _normalize_text(getattr(row, "title", ""), max_len=120), "summary": _normalize_text(getattr(row, "summary", ""), max_len=220), "reasoning": _normalize_text(getattr(row, "reasoning", ""), max_len=220), "status": _normalize_text(getattr(row, "status", ""), max_len=40), "source": _normalize_text(getattr(row, "source", ""), max_len=120), } for row in signal_rows if _normalize_text(getattr(row, "title", ""), max_len=120) ] analysis_object_refs = _build_analysis_object_refs(tasks=tasks, signals=signals) return { "project_name": _normalize_text( getattr(project_row, "title", None) or getattr(project_row, "name", ""), max_len=120, ), "project_description": _normalize_text(getattr(project_row, "description", ""), max_len=220), "task_titles": [item["title"] for item in tasks], "signal_titles": [item["title"] for item in signals], "tasks": tasks, "signals": signals, "analysis_object_refs": analysis_object_refs, } __all__ = ["load_analysis_project_snapshot"]