diff --git a/backend/steering/graph/execution_engine.py b/backend/steering/graph/execution_engine.py index de95f95..5b436d5 100644 --- a/backend/steering/graph/execution_engine.py +++ b/backend/steering/graph/execution_engine.py @@ -111,6 +111,77 @@ def compute_planning_debt( return debts +def planning_debt_to_attention_items( + *, + initiative_id: str, + debts: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """Map Planning Debt read model → Attention Items (AP1.16d).""" + items: list[dict[str, Any]] = [] + for debt in debts: + gate_id = str(debt["roadmap_item_id"]) + items.append( + { + "kind": "planning_debt", + "severity": "warning", + "title": debt.get("title") or "Zielzustand", + "summary": debt.get("message") or "Durchführungsplan fehlt", + "scope_type": "milestone", + "scope_id": gate_id, + "initiative_id": initiative_id, + "action_id": None, + "blocker_id": None, + "milestone_id": gate_id, + "reason_code": "planning_debt", + "data_source": "execution_graph", + } + ) + return items + + +def execution_waiting_to_attention_items( + *, + initiative_id: str, + actions: list[dict[str, Any]], + graph_state: dict[str, Any], +) -> list[dict[str, Any]]: + """Actions blocked by Execution-Graph (nicht Status=blocked) → Attention.""" + action_by_id = {str(action["id"]): action for action in actions} + items: list[dict[str, Any]] = [] + for action_id in graph_state.get("blocked_actions", []): + action = action_by_id.get(str(action_id)) + if not action: + continue + if action.get("status") in ("done", "discarded", "blocked"): + continue + meta = graph_state.get("items", {}).get(str(action_id), {}) + blocked_by = meta.get("blocked_by") or [] + summary = ( + "Wartet auf Vorgänger-Arbeitspaket" + if len(blocked_by) == 1 + else f"Wartet auf {len(blocked_by)} Vorgänger" + ) + items.append( + { + "kind": "execution_waiting", + "severity": "info", + "title": action.get("title") or "Arbeitspaket", + "summary": summary, + "scope_type": "action", + "scope_id": str(action_id), + "initiative_id": initiative_id, + "action_id": str(action_id), + "blocker_id": None, + "milestone_id": ( + str(action["roadmap_item_id"]) if action.get("roadmap_item_id") else None + ), + "reason_code": "execution_waiting", + "data_source": "execution_graph", + } + ) + return items + + def compute_execution_graph_state( *, actions: list[dict[str, Any]], diff --git a/backend/steering/signals/default_rules.py b/backend/steering/signals/default_rules.py index fb65041..43888a4 100644 --- a/backend/steering/signals/default_rules.py +++ b/backend/steering/signals/default_rules.py @@ -18,6 +18,8 @@ AttentionKind = Literal[ "stale_initiative", "milestone_at_risk", "gate_graph_blocked", + "planning_debt", + "execution_waiting", "overdue_action", "review_due", "recurring_due", @@ -316,6 +318,61 @@ def _graph_blocked_gates(cur, ctx: TenantContext) -> list[dict[str, Any]]: return items +def _execution_plan_attention(cur, ctx: TenantContext) -> list[dict[str, Any]]: + """Planning Debt + Execution-Waiting — AP1.16d / Execution-Graph.""" + from services import actions as action_service + from services import roadmap as roadmap_service + from steering.graph.execution_engine import ( + compute_execution_graph_state, + compute_planning_debt, + execution_waiting_to_attention_items, + planning_debt_to_attention_items, + ) + from services.execution_plan import list_dependencies_for_initiative + + cur.execute( + """ + SELECT id FROM initiatives + WHERE tenant_id = %s AND status IN ('active', 'paused') + ORDER BY updated_at DESC + LIMIT 30 + """, + (ctx.tenant_id,), + ) + + items: list[dict[str, Any]] = [] + for row in cur.fetchall(): + initiative_id = str(row["id"]) + actions = action_service.list_actions_for_initiative( + tenant_id=ctx.tenant_id, initiative_id=initiative_id + ) + roadmap_items = roadmap_service.list_roadmap_items_for_initiative( + tenant_id=ctx.tenant_id, initiative_id=initiative_id + ) + debts = compute_planning_debt(actions=actions, roadmap_items=roadmap_items) + 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 + ) + items.extend( + execution_waiting_to_attention_items( + initiative_id=initiative_id, + actions=actions, + graph_state=graph_state, + ) + ) + + if len(items) >= 30: + return items[:30] + return items + + def _milestones_at_risk(cur, ctx: TenantContext) -> list[dict[str, Any]]: cur.execute( """ @@ -491,6 +548,7 @@ def get_attention_items(ctx: TenantContext) -> list[dict[str, Any]]: items.extend(_initiatives_without_next_action(cur, ctx)) items.extend(_stale_initiatives(cur, ctx)) items.extend(_graph_blocked_gates(cur, ctx)) + items.extend(_execution_plan_attention(cur, ctx)) items.extend(_milestones_at_risk(cur, ctx)) items.extend(_overdue_actions(cur, ctx)) items.extend(_actions_review_required(cur, ctx)) diff --git a/backend/tests/test_ap16_execution_graph.py b/backend/tests/test_ap16_execution_graph.py index e16669e..04269a8 100644 --- a/backend/tests/test_ap16_execution_graph.py +++ b/backend/tests/test_ap16_execution_graph.py @@ -3,6 +3,8 @@ from steering.graph.execution_engine import ( compute_execution_graph_state, compute_planning_debt, + execution_waiting_to_attention_items, + planning_debt_to_attention_items, ) @@ -114,3 +116,45 @@ def test_scope_filters_to_gate(): scope_roadmap_item_id="g1", ) assert set(state["items"].keys()) == {"a"} + + +def test_planning_debt_attention_items(): + items = planning_debt_to_attention_items( + initiative_id="init-1", + debts=[ + { + "roadmap_item_id": "gate-1", + "title": "G5 Plan", + "message": "Aktiver Zielzustand ohne Durchführungsplan", + } + ], + ) + assert len(items) == 1 + assert items[0]["kind"] == "planning_debt" + assert items[0]["initiative_id"] == "init-1" + assert items[0]["milestone_id"] == "gate-1" + + +def test_execution_waiting_attention_items(): + actions = [ + _action("a", "open", sort_order=0), + _action("b", "open", sort_order=1), + ] + state = compute_execution_graph_state( + actions=actions, + dependencies=[ + { + "predecessor_action_id": "a", + "successor_action_id": "b", + "dependency_kind": "requires", + } + ], + ) + items = execution_waiting_to_attention_items( + initiative_id="init-1", + actions=actions, + graph_state=state, + ) + assert len(items) == 1 + assert items[0]["kind"] == "execution_waiting" + assert items[0]["action_id"] == "b" diff --git a/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md b/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md index e02eb97..cfda666 100644 --- a/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md +++ b/docs/product/Kairo_Corrected_MVP_Roadmap_v0.2.md @@ -128,7 +128,7 @@ Phase H2 PM Work Modes AP1.9 ✓ 9a / → 9b–9e Phase H3 Plan Outline AP1.12 + AP1.10 ◐ 12a–d, 10c Phase I Gate-Graph AP1.13 + AP1.15 ◐ 13a/b, 15a–c Phase I2 Plan/Ist Snapshots AP1.14 ✓ -Phase I3 Execution-Plan AP1.16 ◐ 16a–b ✓ / 16c UI / 16d offen +Phase I3 Execution-Plan AP1.16 ◐ 16a–c ✓ / 16d ◐ / AP1.9c ◐ Phase J Portfolio AP1.8 ◐ 8a ✓ / 8b deferred Phase K Archetyp-Steuerung AP2.0 ◐ 2.0a–c ✓ / → 2.0d–f Phase L Agent Interface AP1.7 ○ @@ -180,7 +180,7 @@ Siehe **`Kairo_Status_Review_and_Next_Steps_v0.1.md` §5** für vollständige Ro |-------|-------|--------|------------| | D0 | DOC-Sync (Truth Table, Gap, Review) | ✓ | eine Wahrheit | | D1 | Dogfooding R1 — Kairo-Jinkendo in Kairo | **→ nächstes** | B2b-Validation | -| 1 | AP1.9c Cockpit-Signale | offen | MVP §5.7 | +| 1 | AP1.9c Cockpit-Signale | **◐ Code** | Portfolio-Kacheln via Attention | | 2 | AP1.16a–b Execution-Graph (Schema + Engine) | **◐ Code** | **vor** AP2.0d; Remote-Verifikation nach Deploy | | 3 | AP2.0d Next-Action-Strategien | offen | MVP Stufe A; nutzt `ready_actions` | | 4 | AP1.16c–d Plan-Outline-Kanten + Planning Debt | offen | nach 16a–b | diff --git a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md index 3eb417b..b2591f4 100644 --- a/docs/product/Kairo_Implementation_Truth_Table_v0.1.md +++ b/docs/product/Kairo_Implementation_Truth_Table_v0.1.md @@ -65,7 +65,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. | Initiative Archetypes | ◐ | Migration 016, Registry AP2.0b | | Method Profiles (Code-Seeds) | ◐ | `product.kairo_dev`, Kumite, Buch — AP2.0b | | Execution Graph Engine | ◐ | AP1.16b; API `/execution/graph-state` | -| Planning Debt (Attention) | ◐ | Read Model AP1.16b; Cockpit AP1.16d offen | +| Planning Debt (Attention) | ◐ | AP1.16d: Attention + Cockpit-Kacheln AP1.9c | | `work_cycle` / Sprint | ✗ | 📄 MVP v0.3 B3; Migration AP2.0f geplant | --- @@ -99,7 +99,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden. | Sicht | Stand | Anmerkung | |-------|-------|-----------| | PM Work Modes Shell | ✓ | AP1.9a: Cockpit, Work, Plan, Control + Redirects | -| Cockpit (Portfolio) | ◐ | Widget-Grid; Rang AP1.8a; Signale auf Kacheln ✗ AP1.9c | +| Cockpit (Portfolio) | ◐ | Widget-Grid; Rang AP1.8a; Signale AP1.9c ◐ | | Ausführen (/work) | ◐ | Heute, Meine APs | | Planen (/plan) | ◐ | Outline AP1.12, Gates, Inbox, Struktur, Profil | | Kontrolle (/control) | ◐ | Status, Plan/Ist AP1.14, Journey AP1.6b | diff --git a/docs/sprints/Sprint1_AP1_16_Execution_Plan_v0.1.md b/docs/sprints/Sprint1_AP1_16_Execution_Plan_v0.1.md index 3e1ede8..7150e06 100644 --- a/docs/sprints/Sprint1_AP1_16_Execution_Plan_v0.1.md +++ b/docs/sprints/Sprint1_AP1_16_Execution_Plan_v0.1.md @@ -69,21 +69,22 @@ Kanten-Richtung in DB: `predecessor_action_id` → `successor_action_id` (Vorgä | 9 | `GET/POST/DELETE …/execution/dependencies` | | 10 | pytest: Engine + Cycle-Detection (Unit) | -### AP1.16c — Frontend (folgt) +### AP1.16c — Frontend ✓ | # | Inhalt | |---|--------| | 11 | Plan-Outline „Arbeit“: blocked/ready Badges | -| 12 | Vorgänger-Kanten (Liste oder Mini-Graph) | -| 13 | Dependency anlegen/löschen im Action-Kontext | +| 12 | Action-Detail: Vorgänger-Verwaltung | +| 13 | Planning Debt Banner auf Plan → Arbeit | -### AP1.16d — Steuerung (folgt) +### AP1.16d + AP1.9c — Attention & Cockpit ◐ | # | Inhalt | |---|--------| -| 14 | Method Profile: `planning_levels`, `planning_mode` | -| 15 | Planning Debt in Attention / Cockpit | -| 16 | AP2.0d: Next-Action nutzt `ready_actions` | +| 14 | Attention: `planning_debt`, `execution_waiting` | +| 15 | Cockpit Portfolio-Kacheln: Attention-Chips (AP1.9c) | +| 16 | Method Profile `planning_levels` | deferred | +| 17 | AP2.0d: Next-Action nutzt `ready_actions` | offen | --- diff --git a/frontend/src/components/InitiativePortfolioSignalChips.jsx b/frontend/src/components/InitiativePortfolioSignalChips.jsx new file mode 100644 index 0000000..98a5308 --- /dev/null +++ b/frontend/src/components/InitiativePortfolioSignalChips.jsx @@ -0,0 +1,27 @@ +import { attentionKindLabel, topAttentionSignals } from '../utils/attentionSignals.js' + +export function InitiativePortfolioSignalChips({ signals = [] }) { + const top = topAttentionSignals(signals, 2) + if (top.length === 0) { + return null + } + + return ( +