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:
@@ -6,9 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session
|
||||
from api.models.task import Task
|
||||
from api.models.workstream import Workstream
|
||||
from api.models.workstream_dependency import WorkstreamDependency
|
||||
from api.schemas.workstream_dependency import WorkstreamDependencyCreate, WorkstreamDependencyRead
|
||||
from api.models.workplan import Workplan
|
||||
from api.models.workplan_dependency import WorkplanDependency
|
||||
from api.schemas.workplan_dependency import WorkplanDependencyCreate, WorkplanDependencyRead
|
||||
from api.routers.workstreams import _legacy_key, _meter_legacy_route
|
||||
|
||||
router = APIRouter(prefix="/workstreams", tags=["dependencies"])
|
||||
@@ -17,28 +17,28 @@ workplan_router = APIRouter(prefix="/workplans", tags=["dependencies"])
|
||||
|
||||
async def _create_dependency(
|
||||
*,
|
||||
workstream_id: uuid.UUID,
|
||||
body: WorkstreamDependencyCreate,
|
||||
workplan_id: uuid.UUID,
|
||||
body: WorkplanDependencyCreate,
|
||||
session: AsyncSession,
|
||||
) -> WorkstreamDependency:
|
||||
if await session.get(Workstream, workstream_id) is None:
|
||||
) -> WorkplanDependency:
|
||||
if await session.get(Workplan, workplan_id) is None:
|
||||
raise HTTPException(status_code=404, detail="from workplan not found")
|
||||
|
||||
has_workstream_target = body.to_workstream_id is not None
|
||||
has_workplan_target = body.to_workplan_id is not None
|
||||
has_task_target = body.to_task_id is not None
|
||||
if has_workstream_target == has_task_target:
|
||||
if has_workplan_target == has_task_target:
|
||||
raise HTTPException(status_code=422, detail="provide exactly one dependency target")
|
||||
|
||||
if body.to_workstream_id and await session.get(Workstream, body.to_workstream_id) is None:
|
||||
if body.to_workplan_id and await session.get(Workplan, body.to_workplan_id) is None:
|
||||
raise HTTPException(status_code=404, detail="target workplan not found")
|
||||
if body.to_task_id and await session.get(Task, body.to_task_id) is None:
|
||||
raise HTTPException(status_code=404, detail="target task not found")
|
||||
if workstream_id == body.to_workstream_id:
|
||||
if workplan_id == body.to_workplan_id:
|
||||
raise HTTPException(status_code=422, detail="a workplan cannot depend on itself")
|
||||
|
||||
dep = WorkstreamDependency(
|
||||
from_workstream_id=workstream_id,
|
||||
to_workstream_id=body.to_workstream_id,
|
||||
dep = WorkplanDependency(
|
||||
from_workplan_id=workplan_id,
|
||||
to_workplan_id=body.to_workplan_id,
|
||||
to_task_id=body.to_task_id,
|
||||
relationship_type=body.relationship_type,
|
||||
description=body.description,
|
||||
@@ -51,15 +51,15 @@ async def _create_dependency(
|
||||
|
||||
async def _list_dependencies(
|
||||
*,
|
||||
workstream_id: uuid.UUID,
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
) -> list[WorkstreamDependency]:
|
||||
if await session.get(Workstream, workstream_id) is None:
|
||||
) -> list[WorkplanDependency]:
|
||||
if await session.get(Workplan, workplan_id) is None:
|
||||
raise HTTPException(status_code=404, detail="workplan not found")
|
||||
rows = await session.execute(
|
||||
select(WorkstreamDependency).where(
|
||||
(WorkstreamDependency.from_workstream_id == workstream_id)
|
||||
| (WorkstreamDependency.to_workstream_id == workstream_id)
|
||||
select(WorkplanDependency).where(
|
||||
(WorkplanDependency.from_workplan_id == workplan_id)
|
||||
| (WorkplanDependency.to_workplan_id == workplan_id)
|
||||
)
|
||||
)
|
||||
return list(rows.scalars().all())
|
||||
@@ -67,14 +67,14 @@ async def _list_dependencies(
|
||||
|
||||
async def _delete_dependency(
|
||||
*,
|
||||
workstream_id: uuid.UUID,
|
||||
workplan_id: uuid.UUID,
|
||||
dep_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
dep = await session.get(WorkstreamDependency, dep_id)
|
||||
dep = await session.get(WorkplanDependency, dep_id)
|
||||
if dep is None:
|
||||
raise HTTPException(status_code=404, detail="dependency not found")
|
||||
if dep.from_workstream_id != workstream_id:
|
||||
if dep.from_workplan_id != workplan_id:
|
||||
raise HTTPException(status_code=403, detail="dependency does not belong to this workplan")
|
||||
await session.delete(dep)
|
||||
await session.commit()
|
||||
@@ -82,17 +82,17 @@ async def _delete_dependency(
|
||||
|
||||
@router.post(
|
||||
"/{workstream_id}/dependencies/",
|
||||
response_model=WorkstreamDependencyRead,
|
||||
response_model=WorkplanDependencyRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_dependency(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
body: WorkstreamDependencyCreate,
|
||||
body: WorkplanDependencyCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> WorkstreamDependency:
|
||||
"""Record that workstream_id depends on another workstream or a task."""
|
||||
) -> WorkplanDependency:
|
||||
"""Record that workstream_id depends on another workplan or a task."""
|
||||
await _meter_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
@@ -100,33 +100,33 @@ async def create_dependency(
|
||||
interface_key=_legacy_key("POST", "/workstreams/{workstream_id}/dependencies/"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/",
|
||||
)
|
||||
return await _create_dependency(workstream_id=workstream_id, body=body, session=session)
|
||||
return await _create_dependency(workplan_id=workstream_id, body=body, session=session)
|
||||
|
||||
|
||||
@workplan_router.post(
|
||||
"/{workplan_id}/dependencies/",
|
||||
response_model=WorkstreamDependencyRead,
|
||||
response_model=WorkplanDependencyRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_workplan_dependency(
|
||||
workplan_id: uuid.UUID,
|
||||
body: WorkstreamDependencyCreate,
|
||||
body: WorkplanDependencyCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> WorkstreamDependency:
|
||||
return await _create_dependency(workstream_id=workplan_id, body=body, session=session)
|
||||
) -> WorkplanDependency:
|
||||
return await _create_dependency(workplan_id=workplan_id, body=body, session=session)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{workstream_id}/dependencies/",
|
||||
response_model=list[WorkstreamDependencyRead],
|
||||
response_model=list[WorkplanDependencyRead],
|
||||
)
|
||||
async def list_dependencies(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[WorkstreamDependency]:
|
||||
"""Return all dependency edges touching this workstream (both directions)."""
|
||||
) -> list[WorkplanDependency]:
|
||||
"""Return all dependency edges touching this workplan (both directions)."""
|
||||
await _meter_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
@@ -134,18 +134,18 @@ async def list_dependencies(
|
||||
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}/dependencies/"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/",
|
||||
)
|
||||
return await _list_dependencies(workstream_id=workstream_id, session=session)
|
||||
return await _list_dependencies(workplan_id=workstream_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.get(
|
||||
"/{workplan_id}/dependencies/",
|
||||
response_model=list[WorkstreamDependencyRead],
|
||||
response_model=list[WorkplanDependencyRead],
|
||||
)
|
||||
async def list_workplan_dependencies(
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[WorkstreamDependency]:
|
||||
return await _list_dependencies(workstream_id=workplan_id, session=session)
|
||||
) -> list[WorkplanDependency]:
|
||||
return await _list_dependencies(workplan_id=workplan_id, session=session)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -167,7 +167,7 @@ async def delete_dependency(
|
||||
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}/dependencies/{dep_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/{dep_id}",
|
||||
)
|
||||
await _delete_dependency(workstream_id=workstream_id, dep_id=dep_id, session=session)
|
||||
await _delete_dependency(workplan_id=workstream_id, dep_id=dep_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.delete(
|
||||
@@ -179,4 +179,4 @@ async def delete_workplan_dependency(
|
||||
dep_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await _delete_dependency(workstream_id=workplan_id, dep_id=dep_id, session=session)
|
||||
await _delete_dependency(workplan_id=workplan_id, dep_id=dep_id, session=session)
|
||||
Reference in New Issue
Block a user