"""Execution plan API — AP1.16b.""" from __future__ import annotations from typing import Literal, Optional from capabilities import require_capability from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from services import actions as action_service from services import execution_plan as execution_plan_service from steering.graph.execution_engine import load_initiative_execution_graph_state from tenant_context import TenantContext initiative_router = APIRouter(prefix="/api/initiatives", tags=["execution-plan"]) class ActionDependencyCreateRequest(BaseModel): predecessor_action_id: str successor_action_id: str dependency_kind: Literal["requires", "blocks", "relates"] = "requires" @initiative_router.get("/{initiative_id}/execution/graph-state") def get_initiative_execution_graph_state( initiative_id: str, scope_roadmap_item_id: Optional[str] = Query(default=None), ctx: TenantContext = Depends(require_capability("kairo.action.read")), ): try: state = load_initiative_execution_graph_state( tenant_id=ctx.tenant_id, initiative_id=initiative_id, scope_roadmap_item_id=scope_roadmap_item_id, ) if scope_roadmap_item_id is None: from services import actions as action_service from services import roadmap as roadmap_service all_actions = action_service.list_actions_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) roadmap_items = roadmap_service.list_roadmap_items_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) from steering.graph.execution_engine import compute_planning_debt state["planning_debt"] = compute_planning_debt( actions=all_actions, roadmap_items=roadmap_items, ) return state except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @initiative_router.get("/{initiative_id}/execution/dependencies") def list_initiative_action_dependencies( initiative_id: str, ctx: TenantContext = Depends(require_capability("kairo.action.read")), ): try: return execution_plan_service.list_dependencies_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @initiative_router.post("/{initiative_id}/execution/dependencies", status_code=201) def create_initiative_action_dependency( initiative_id: str, body: ActionDependencyCreateRequest, ctx: TenantContext = Depends(require_capability("kairo.action.manage")), ): try: predecessor = action_service.get_action( tenant_id=ctx.tenant_id, action_id=body.predecessor_action_id ) successor = action_service.get_action( tenant_id=ctx.tenant_id, action_id=body.successor_action_id ) if not predecessor or not successor: raise ValueError("Action nicht gefunden") if ( predecessor["initiative_id"] != initiative_id or successor["initiative_id"] != initiative_id ): raise ValueError("Actions gehören nicht zum angegebenen Vorhaben") return execution_plan_service.add_dependency( tenant_id=ctx.tenant_id, predecessor_action_id=body.predecessor_action_id, successor_action_id=body.successor_action_id, dependency_kind=body.dependency_kind, user_id=ctx.user_id, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @initiative_router.delete( "/{initiative_id}/execution/dependencies/{dependency_id}", status_code=204, ) def delete_initiative_action_dependency( initiative_id: str, dependency_id: str, ctx: TenantContext = Depends(require_capability("kairo.action.manage")), ): del initiative_id # scope validated via tenant + dependency row try: execution_plan_service.delete_dependency( tenant_id=ctx.tenant_id, dependency_id=dependency_id, user_id=ctx.user_id, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc