"""Recurring control NextAction strategy — AP2.0d.""" from __future__ import annotations from typing import Any from db import get_connection from psycopg2.extras import RealDictCursor from steering.signals import default_rules from steering.strategies.next_action.default_strategy import DefaultNextActionStrategy from steering.strategies.next_action.execution_ready import ( list_execution_ready_candidates, merge_candidates, ) from steering.strategies.next_action.registry import ( get_next_action_strategy, register_next_action_strategy, ) from tenant_context import TenantContext _default = DefaultNextActionStrategy() def _recurring_due_candidates( ctx: TenantContext, initiative_id: str, *, limit: int ) -> list[dict[str, Any]]: conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT 'recurring_due' AS kind, re.title AS title, 'Wiederkehrendes Element fällig' AS summary, re.initiative_id, NULL::uuid AS action_id, NULL::uuid AS backlog_item_id, 'recurring_due' AS reason_code, 'Rhythmus-Element bearbeiten' AS recommended_action FROM recurring_elements re WHERE re.tenant_id = %s AND re.initiative_id = %s AND re.status = 'active' AND re.next_due_at IS NOT NULL AND re.next_due_at <= NOW() ORDER BY re.next_due_at ASC LIMIT %s """, (ctx.tenant_id, initiative_id, limit), ) items: list[dict[str, Any]] = [] for row in cur.fetchall(): item = dict(row) item["initiative_id"] = str(item["initiative_id"]) items.append(item) return items finally: conn.close() class RecurringControlStrategy: key = "recurring_control" 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) recurring = _recurring_due_candidates(ctx, initiative_id, limit=limit) remaining = limit - len(recurring) ready: list[dict[str, Any]] = [] if remaining > 0: ready = list_execution_ready_candidates( ctx, initiative_id, limit=remaining, prefer_critical_path=False ) merged = merge_candidates(recurring, ready, limit=limit) if len(merged) >= limit: return merged[:limit] rest = default_rules.get_next_action_candidates_for_initiative( ctx, initiative_id=initiative_id, limit=limit - len(merged) ) return merge_candidates(merged, rest, limit=limit) recurring_control_strategy = RecurringControlStrategy() def register_recurring_control_strategy() -> None: if not get_next_action_strategy(recurring_control_strategy.key): register_next_action_strategy(recurring_control_strategy)