"""Agile iteration NextAction strategy — AP2.0f (active work_cycle first).""" from __future__ import annotations from typing import Any from steering.signals import default_rules from steering.strategies.next_action.continuous_product import ( ContinuousProductStrategy, ) from steering.strategies.next_action.default_strategy import DefaultNextActionStrategy from steering.strategies.next_action.execution_ready import ( blocked_execution_action_ids, build_execution_ready_candidates, list_execution_ready_candidates, merge_candidates, ) from steering.strategies.next_action.registry import ( get_next_action_strategy, register_next_action_strategy, ) from services.work_cycle import get_active_work_cycle from tenant_context import TenantContext _default = DefaultNextActionStrategy() _continuous = ContinuousProductStrategy() class AgileIterationStrategy: key = "agile_iteration" def evaluate( self, ctx: TenantContext, *, initiative_id: str | None = None, limit: int = 10, ) -> list[dict[str, Any]]: if limit < 1: limit = 1 if not initiative_id: return _default.evaluate(ctx, limit=limit) active = get_active_work_cycle( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) if not active: return _continuous.evaluate(ctx, initiative_id=initiative_id, limit=limit) cycle_id = active["id"] from services import actions as action_service from steering.graph.execution_engine import load_initiative_execution_graph_state actions = action_service.list_actions_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) cycle_actions = [ a for a in actions if str(a.get("work_cycle_id") or "") == cycle_id ] if not cycle_actions: ready = list_execution_ready_candidates( ctx, initiative_id, limit=limit, prefer_critical_path=True ) if ready: return ready[:limit] return _continuous.evaluate(ctx, initiative_id=initiative_id, limit=limit) graph_state = load_initiative_execution_graph_state( tenant_id=ctx.tenant_id, initiative_id=initiative_id, ) blocked_ids = blocked_execution_action_ids(ctx, initiative_id) ready = build_execution_ready_candidates( actions=cycle_actions, graph_state=graph_state, limit=limit, prefer_critical_path=True, ) for item in ready: item["reason_code"] = "work_cycle_ready" item["summary"] = f"Aktive Zeitbox „{active['title']}“ — ausführungsbereit" item["recommended_action"] = "In dieser Iteration ausführen" if len(ready) >= limit: return ready[:limit] rest = _continuous.evaluate( ctx, initiative_id=initiative_id, limit=limit - len(ready) ) filtered = [ item for item in rest if not item.get("action_id") or item["action_id"] not in blocked_ids ] return merge_candidates(ready, filtered, limit=limit, exclude_action_ids=blocked_ids) agile_iteration_strategy = AgileIterationStrategy() def register_agile_iteration_strategy() -> None: if not get_next_action_strategy(agile_iteration_strategy.key): register_next_action_strategy(agile_iteration_strategy)