"""Attention evaluation (Q5) — method- and horizon-aware for one initiative.""" from __future__ import annotations from typing import Any from steering.kernel.models import HorizonMarker, SteeringBinding from steering.strategies.next_action.execution_ready import gate_horizon_scope_for_method from tenant_context import TenantContext _GATE_METHODS = frozenset( { "sequential_dependency", "program_delivery", "maturity_progression", } ) def evaluate_attention( ctx: TenantContext, *, initiative_id: str, binding: SteeringBinding, horizon: HorizonMarker, next_work: list[dict[str, Any]], limit: int = 20, ) -> list[dict[str, Any]]: """Kernel attention for a single initiative — no fake next when planning debt.""" from services import actions as action_service from services import blockers as blocker_service from services import roadmap as roadmap_service from services.execution_plan import list_dependencies_for_initiative from steering.graph.execution_engine import ( compute_execution_graph_state, compute_planning_debt, execution_waiting_to_attention_items, planning_debt_to_attention_items, ) items: list[dict[str, Any]] = [] blockers = blocker_service.list_blockers_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) for blocker in blockers: if blocker.get("status") not in ("open", "in_progress"): continue items.append( { "kind": "open_blocker", "severity": "warning", "title": blocker.get("title") or "Blocker", "summary": "Offener Blocker im Vorhaben", "scope_type": "blocker", "scope_id": str(blocker["id"]), "initiative_id": initiative_id, "action_id": ( str(blocker["action_id"]) if blocker.get("action_id") else None ), "blocker_id": str(blocker["id"]), "reason_code": "path_blocked", "data_source": "steering_kernel", } ) actions = action_service.list_actions_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) for action in actions: if action.get("status") != "blocked": continue items.append( { "kind": "blocked_action", "severity": "critical", "title": action.get("title") or "Arbeitspaket", "summary": "Maßnahme ist blockiert", "scope_type": "action", "scope_id": str(action["id"]), "initiative_id": initiative_id, "action_id": str(action["id"]), "reason_code": "path_blocked", "data_source": "steering_kernel", } ) gate_scope = gate_horizon_scope_for_method( binding.primary_method_key, ctx, initiative_id ) roadmap_items = roadmap_service.list_roadmap_items_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) scoped_roadmap = roadmap_items scoped_actions = actions if gate_scope: scope = str(gate_scope) scoped_roadmap = [ri for ri in roadmap_items if str(ri.get("id")) == scope] scoped_actions = [ a for a in actions if a.get("roadmap_item_id") and str(a["roadmap_item_id"]) == scope ] if binding.primary_method_key in _GATE_METHODS: debts = compute_planning_debt( actions=scoped_actions, roadmap_items=scoped_roadmap ) items.extend( planning_debt_to_attention_items( initiative_id=initiative_id, debts=debts ) ) dependencies = list_dependencies_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id ) graph_state = compute_execution_graph_state( actions=actions, dependencies=dependencies, scope_roadmap_item_id=gate_scope, ) items.extend( execution_waiting_to_attention_items( initiative_id=initiative_id, actions=actions, graph_state=graph_state, ) ) if ( not next_work and binding.primary_method_key in _GATE_METHODS and horizon.kind == "gate" ): has_planning = any(i.get("reason_code") == "planning_debt" for i in items) has_waiting = any(i.get("reason_code") == "execution_waiting" for i in items) if not has_planning and not has_waiting and not graph_state.get("ready_actions"): items.append( { "kind": "initiative_without_next_action", "severity": "info", "title": horizon.gate_title or "Aktiver Horizont", "summary": "Keine ausführungsbereite Arbeit im aktiven Horizont", "scope_type": "milestone", "scope_id": horizon.gate_roadmap_item_id, "initiative_id": initiative_id, "reason_code": "horizon_no_ready_work", "data_source": "steering_kernel", } ) for item in items: item.setdefault("data_source", "steering_kernel") items.sort( key=lambda x: {"critical": 0, "warning": 1, "info": 2}.get( x.get("severity", "info"), 9 ) ) return items[:limit]