generated from coulomb/repo-seed
S0 — Design boundary formalised across all integration surfaces:
- TOOLS.md restructured with Design Boundary section, Sanctioned Write Tools,
and Bootstrap-Only Tools (create_workstream, create_task) with explicit note
- project_claude_md.template and railiance CLAUDE.md updated with boundary note
and get_next_steps() in session start protocol
- Global ~/.claude/CLAUDE.md updated accordingly
S1 — Workstream dependency graph:
- WorkstreamDependency model (directed edge, CASCADE on delete, unique pair constraint)
- Alembic migration 0b547c153153; script.py.mako added (was missing)
- REST API: POST/GET /workstreams/{id}/dependencies/, DELETE …/{dep_id} (hard delete)
- StateSummary open_workstreams enriched with depends_on/blocks lists
- MCP tools: create_dependency(), list_dependencies()
- Dashboard workstreams page: Dependencies section with relationship cards
- Seeded: custodian-agent-runtime → llm-shared-library + phase-0-operational-baseline
S2 — Suggesting Next Steps (sanctioned write use case #2):
- GET /state/next_steps derives suggestions from recently resolved decisions
(→ first open task in same workstream) and cleared dependencies
(→ first todo task in now-unblocked workstream)
- StateSummary.next_steps included on every summary call
- MCP tool: get_next_steps()
- Dashboard: "What's next?" card grid above Registered Projects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.database import get_session
|
|
from api.models.workstream import Workstream
|
|
from api.models.workstream_dependency import WorkstreamDependency
|
|
from api.schemas.workstream_dependency import WorkstreamDependencyCreate, WorkstreamDependencyRead
|
|
|
|
router = APIRouter(prefix="/workstreams", tags=["dependencies"])
|
|
|
|
|
|
@router.post(
|
|
"/{workstream_id}/dependencies/",
|
|
response_model=WorkstreamDependencyRead,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
async def create_dependency(
|
|
workstream_id: uuid.UUID,
|
|
body: WorkstreamDependencyCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> WorkstreamDependency:
|
|
"""Record that workstream_id depends on body.to_workstream_id."""
|
|
if await session.get(Workstream, workstream_id) is None:
|
|
raise HTTPException(status_code=404, detail="from workstream not found")
|
|
if await session.get(Workstream, body.to_workstream_id) is None:
|
|
raise HTTPException(status_code=404, detail="to workstream not found")
|
|
if workstream_id == body.to_workstream_id:
|
|
raise HTTPException(status_code=422, detail="a workstream cannot depend on itself")
|
|
|
|
dep = WorkstreamDependency(
|
|
from_workstream_id=workstream_id,
|
|
to_workstream_id=body.to_workstream_id,
|
|
description=body.description,
|
|
)
|
|
session.add(dep)
|
|
await session.commit()
|
|
await session.refresh(dep)
|
|
return dep
|
|
|
|
|
|
@router.get(
|
|
"/{workstream_id}/dependencies/",
|
|
response_model=list[WorkstreamDependencyRead],
|
|
)
|
|
async def list_dependencies(
|
|
workstream_id: uuid.UUID,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> list[WorkstreamDependency]:
|
|
"""Return all dependency edges touching this workstream (both directions)."""
|
|
if await session.get(Workstream, workstream_id) is None:
|
|
raise HTTPException(status_code=404, detail="workstream not found")
|
|
rows = await session.execute(
|
|
select(WorkstreamDependency).where(
|
|
(WorkstreamDependency.from_workstream_id == workstream_id)
|
|
| (WorkstreamDependency.to_workstream_id == workstream_id)
|
|
)
|
|
)
|
|
return list(rows.scalars().all())
|
|
|
|
|
|
@router.delete(
|
|
"/{workstream_id}/dependencies/{dep_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
)
|
|
async def delete_dependency(
|
|
workstream_id: uuid.UUID,
|
|
dep_id: uuid.UUID,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> None:
|
|
"""Hard-delete a dependency edge. Removing a constraint is safe — no information is lost."""
|
|
dep = await session.get(WorkstreamDependency, dep_id)
|
|
if dep is None:
|
|
raise HTTPException(status_code=404, detail="dependency not found")
|
|
if dep.from_workstream_id != workstream_id:
|
|
raise HTTPException(status_code=403, detail="dependency does not belong to this workstream")
|
|
await session.delete(dep)
|
|
await session.commit()
|