Kairo-Jinkendo/backend/routers/execution_plan.py
Lars 1166a0c7f1
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Successful in 2m20s
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 14s
AP1.16a-b: Execution-Graph für Arbeitspaket-Abhängigkeiten.
Migration 023, Engine, API und ADP für Durchführungsplan getrennt vom Gate-Graph; Docs und Sprint-Assignment synchronisiert.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 10:38:01 +02:00

120 lines
4.3 KiB
Python

"""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