2026-03-16 09:13:34 +02:00
|
|
|
"""
|
|
|
|
|
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,
|
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
|
|
|
revise_comment=comment,
|
2026-03-16 09:13:34 +02:00
|
|
|
)
|
|
|
|
|
return {"phase": updated, "new_task": new_task}
|