Kairo-Jinkendo/backend/steering/kernel/attention.py
Lars ef5ee0d19b
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
feat(steering): Epic Roll-up Read Model und Attention (ADP P4)
Schliesst den Backlog/Epic-Track mit kernel-basiertem Fortschritt pro Epic, Steuerungs-Attention und UI-Fortschrittsanzeige im Plan-Eingang ab.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:13:47 +02:00

184 lines
6.2 KiB
Python

"""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,
)
)
from services.operating_context import get_operating_context
from services import backlog as backlog_service
from steering.read_models.epic_rollup import (
compute_epic_rollup,
epic_rollup_attention_items,
)
operating = get_operating_context(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
if operating.get("backlog_vocabulary", {}).get("capabilities", {}).get(
"epic_hierarchy"
):
backlog_items = backlog_service.list_backlog_for_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
)
rollups = compute_epic_rollup(backlog_items=backlog_items, actions=actions)
items.extend(
epic_rollup_attention_items(initiative_id=initiative_id, rollups=rollups)
)
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]