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
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>
120 lines
4.3 KiB
Python
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
|