Kairo-Jinkendo/backend/steering/kernel/attention.py
Lars 20636f88d2
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 4m11s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 15s
feat(steering): Kernel v0.3 mit Read-Model- und Proposal-Registry (K-Ext-1, P6)
Erweitert evaluate_steering um read_models und proposals, migriert epic_rollup auf Registry, liefert sprint_commit-Vorschlaege und UI auf Plan-Sprint.

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

190 lines
6.3 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]],
read_models: dict[str, Any] | None = None,
proposals: dict[str, Any] | None = None,
eval_ctx=None,
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,
)
)
read_models = read_models or {}
proposals = proposals or {}
from steering.read_models.epic_rollup import epic_rollup_attention_items
from steering.proposals.sprint_commit import sprint_commit_attention_items
epic_rollups = read_models.get("epic_rollup")
if epic_rollups:
items.extend(
epic_rollup_attention_items(
initiative_id=initiative_id, rollups=epic_rollups
)
)
sprint_proposals = proposals.get("sprint_commit")
if sprint_proposals:
items.extend(
sprint_commit_attention_items(
initiative_id=initiative_id,
proposals=sprint_proposals,
actions=actions,
)
)
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]