""" Kin — Research Phase Pipeline (KIN-059). Sequential workflow: Director describes a new project, picks researcher roles, each phase produces a task for review. After approve → next phase activates. Architect always runs last (auto-added when any researcher is selected). """ import sqlite3 from core import models # Canonical order of research roles (architect always last) RESEARCH_ROLES = [ "business_analyst", "market_researcher", "legal_researcher", "tech_researcher", "ux_designer", "marketer", "architect", ] # Human-readable labels ROLE_LABELS = { "business_analyst": "Business Analyst", "market_researcher": "Market Researcher", "legal_researcher": "Legal Researcher", "tech_researcher": "Tech Researcher", "ux_designer": "UX Designer", "marketer": "Marketer", "architect": "Architect", } def validate_roles(roles: list[str]) -> list[str]: """Filter unknown roles, remove duplicates, strip 'architect' (auto-added later).""" seen: set[str] = set() result = [] for r in roles: r = r.strip().lower() if r == "architect": continue if r in RESEARCH_ROLES and r not in seen: seen.add(r) result.append(r) return result def build_phase_order(selected_roles: list[str]) -> list[str]: """Return roles in canonical RESEARCH_ROLES order, append architect if any selected.""" ordered = [r for r in RESEARCH_ROLES if r in selected_roles and r != "architect"] if ordered: ordered.append("architect") return ordered def create_project_with_phases( conn: sqlite3.Connection, id: str, name: str, path: str, description: str, selected_roles: list[str], tech_stack: list | None = None, priority: int = 5, language: str = "ru", ) -> dict: """Create project + sequential research phases. Returns {project, phases}. """ clean_roles = validate_roles(selected_roles) ordered_roles = build_phase_order(clean_roles) if not ordered_roles: raise ValueError("At least one research role must be selected") project = models.create_project( conn, id, name, path, tech_stack=tech_stack, priority=priority, language=language, description=description, ) phases = [] for idx, role in enumerate(ordered_roles): phase = models.create_phase(conn, id, role, idx) phases.append(phase) # Activate the first phase immediately if phases: phases[0] = activate_phase(conn, phases[0]["id"]) return {"project": project, "phases": phases} def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict: """Create a task for the phase and set it to active. Task brief includes project description + phase context. """ phase = models.get_phase(conn, phase_id) if not phase: raise ValueError(f"Phase {phase_id} not found") project = models.get_project(conn, phase["project_id"]) if not project: raise ValueError(f"Project {phase['project_id']} not found") task_id = models.next_task_id(conn, phase["project_id"], category=None) brief = { "text": project.get("description") or project["name"], "phase": phase["role"], "phase_order": phase["phase_order"], "workflow": "research", } task = models.create_task( conn, task_id, phase["project_id"], title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}", assigned_role=phase["role"], brief=brief, status="pending", category=None, ) updated = models.update_phase(conn, phase_id, task_id=task["id"], status="active") return updated def approve_phase(conn: sqlite3.Connection, phase_id: int) -> dict: """Approve a phase, activate the next one (or finish workflow). Returns {phase, next_phase|None}. """ phase = models.get_phase(conn, phase_id) if not phase: raise ValueError(f"Phase {phase_id} not found") if phase["status"] != "active": raise ValueError(f"Phase {phase_id} is not active (current: {phase['status']})") updated = models.update_phase(conn, phase_id, status="approved") # Find next pending phase all_phases = models.list_phases(conn, phase["project_id"]) next_phase = None for p in all_phases: if p["phase_order"] > phase["phase_order"] and p["status"] == "pending": next_phase = p break if next_phase: activated = activate_phase(conn, next_phase["id"]) return {"phase": updated, "next_phase": activated} return {"phase": updated, "next_phase": None} def reject_phase(conn: sqlite3.Connection, phase_id: int, reason: str) -> dict: """Reject a phase (director rejects the research output entirely).""" phase = models.get_phase(conn, phase_id) if not phase: raise ValueError(f"Phase {phase_id} not found") if phase["status"] != "active": raise ValueError(f"Phase {phase_id} is not active (current: {phase['status']})") return models.update_phase(conn, phase_id, status="rejected") def revise_phase(conn: sqlite3.Connection, phase_id: int, comment: str) -> dict: """Request revision: create a new task for the same role with the comment. Returns {phase, new_task}. """ phase = models.get_phase(conn, phase_id) if not phase: raise ValueError(f"Phase {phase_id} not found") if phase["status"] not in ("active", "revising"): raise ValueError( f"Phase {phase_id} cannot be revised (current: {phase['status']})" ) project = models.get_project(conn, phase["project_id"]) if not project: raise ValueError(f"Project {phase['project_id']} not found") new_task_id = models.next_task_id(conn, phase["project_id"], category=None) brief = { "text": project.get("description") or project["name"], "phase": phase["role"], "phase_order": phase["phase_order"], "workflow": "research", "revise_comment": comment, "revise_count": (phase.get("revise_count") or 0) + 1, } new_task = models.create_task( conn, new_task_id, phase["project_id"], title=f"[Research Revise] {ROLE_LABELS.get(phase['role'], phase['role'])}", assigned_role=phase["role"], brief=brief, status="pending", category=None, ) new_revise_count = (phase.get("revise_count") or 0) + 1 updated = models.update_phase( conn, phase_id, status="revising", task_id=new_task["id"], revise_count=new_revise_count, ) return {"phase": updated, "new_task": new_task}