generated from coulomb/repo-seed
feat(classification-spine): implement STATE-WP-0065 repo-anchored model
Replace the ad-hoc coordination-domain spine with the Repo Classification Standard: 14 market domains, classification columns on managed_repos, and workplans anchored by repo_id (topic_id optional). - Add Alembic migration d8e9f0a1b2c3 with data backfill and workstream→workplan rename - Add api/classification.py validation and register-from-classification tooling - Expose workplan-first REST/MCP surface with legacy workstream aliases - Add C-24 consistency rule and legacy domain frontmatter mapping - Update dashboard repos page with category/capability/stake filters - Update orientation docs; mark STATE-WP-0065 finished
This commit is contained in:
@@ -9,7 +9,7 @@ from api.database import get_session
|
||||
from api.models.progress_event import ProgressEvent
|
||||
from api.models.task import Task, TaskStatus
|
||||
from api.models.token_event import TokenEvent
|
||||
from api.models.workstream import Workstream
|
||||
from api.models.workplan import Workplan
|
||||
from api.schemas.task import (
|
||||
TaskCountRead,
|
||||
TaskCreate,
|
||||
@@ -26,6 +26,7 @@ router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||
|
||||
@router.get("/", response_model=list[TaskRead])
|
||||
async def list_tasks(
|
||||
workplan_id: uuid.UUID | None = None,
|
||||
workstream_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
assignee: str | None = None,
|
||||
@@ -37,8 +38,9 @@ async def list_tasks(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[Task]:
|
||||
q = select(Task)
|
||||
if workstream_id:
|
||||
q = q.where(Task.workstream_id == workstream_id)
|
||||
scope_id = workplan_id or workstream_id
|
||||
if scope_id:
|
||||
q = q.where(Task.workplan_id == scope_id)
|
||||
if status:
|
||||
q = q.where(Task.status == TaskStatus(normalize_task_status(status)))
|
||||
if assignee:
|
||||
@@ -60,18 +62,20 @@ async def list_tasks(
|
||||
|
||||
@router.get("/counts", response_model=list[TaskCountRead])
|
||||
async def count_tasks(
|
||||
workplan_id: uuid.UUID | None = None,
|
||||
workstream_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[TaskCountRead]:
|
||||
q = select(Task.workstream_id, Task.status, func.count()).group_by(Task.workstream_id, Task.status)
|
||||
if workstream_id:
|
||||
q = q.where(Task.workstream_id == workstream_id)
|
||||
q = select(Task.workplan_id, Task.status, func.count()).group_by(Task.workplan_id, Task.status)
|
||||
scope_id = workplan_id or workstream_id
|
||||
if scope_id:
|
||||
q = q.where(Task.workplan_id == scope_id)
|
||||
if status:
|
||||
q = q.where(Task.status == TaskStatus(normalize_task_status(status)))
|
||||
rows = await session.execute(q)
|
||||
return [
|
||||
TaskCountRead(workstream_id=ws_id, status=task_status, count=count)
|
||||
TaskCountRead(workplan_id=ws_id, status=task_status, count=count)
|
||||
for ws_id, task_status, count in rows
|
||||
]
|
||||
|
||||
@@ -84,7 +88,7 @@ async def create_task(
|
||||
task = Task(**body.model_dump())
|
||||
session.add(task)
|
||||
if status_value(task.status) == "progress":
|
||||
ws = await session.get(Workstream, task.workstream_id)
|
||||
ws = await session.get(Workplan, task.workplan_id)
|
||||
transition_task_status(
|
||||
task,
|
||||
task.status,
|
||||
@@ -137,7 +141,7 @@ async def bulk_status_sync(
|
||||
target_status = status_value(update.status)
|
||||
if update.blocking_reason is not None:
|
||||
task.blocking_reason = update.blocking_reason
|
||||
ws = await session.get(Workstream, task.workstream_id)
|
||||
ws = await session.get(Workplan, task.workplan_id)
|
||||
transition_task_status(
|
||||
task,
|
||||
update.status,
|
||||
@@ -146,7 +150,7 @@ async def bulk_status_sync(
|
||||
)
|
||||
event = ProgressEvent(
|
||||
task_id=task.id,
|
||||
workstream_id=task.workstream_id,
|
||||
workplan_id=task.workplan_id,
|
||||
event_type="task_status_changed",
|
||||
summary=f"Task status -> {target_status}: {task.title}",
|
||||
author=author,
|
||||
@@ -218,7 +222,7 @@ async def update_task(
|
||||
for field, value in update_data.items():
|
||||
setattr(task, field, value)
|
||||
if new_status is not None:
|
||||
ws = await session.get(Workstream, task.workstream_id)
|
||||
ws = await session.get(Workplan, task.workplan_id)
|
||||
transition_task_status(
|
||||
task,
|
||||
status_update,
|
||||
@@ -247,7 +251,7 @@ async def update_task(
|
||||
elif "workplan_tokens_in" in token_data and "workplan_tokens_out" in token_data:
|
||||
# Tier 2: prorate workplan total across task count
|
||||
count_result = await session.execute(
|
||||
select(func.count(Task.id)).where(Task.workstream_id == task.workstream_id)
|
||||
select(func.count(Task.id)).where(Task.workplan_id == task.workplan_id)
|
||||
)
|
||||
task_count = max(count_result.scalar() or 1, 1)
|
||||
tin = token_data["workplan_tokens_in"] // task_count
|
||||
@@ -273,12 +277,12 @@ async def update_task(
|
||||
raw_metadata = {"estimation_method": "fixed_task_done_fallback"}
|
||||
|
||||
# Resolve repo_id via workstream
|
||||
ws = await session.get(Workstream, task.workstream_id)
|
||||
ws = await session.get(Workplan, task.workplan_id)
|
||||
repo_id = ws.repo_id if ws else None
|
||||
|
||||
event = TokenEvent(
|
||||
task_id=task_id,
|
||||
workstream_id=task.workstream_id,
|
||||
workplan_id=task.workplan_id,
|
||||
repo_id=repo_id,
|
||||
tokens_in=tin,
|
||||
tokens_out=tout,
|
||||
|
||||
Reference in New Issue
Block a user