"""Sprint commit proposals — ADP P6 / agile_iteration.""" from __future__ import annotations from typing import Any from steering.eval_context import SteeringEvalContext COMMITTABLE_STATUSES = frozenset({"new", "triaged", "accepted"}) PLANABLE_CYCLE_STATUSES = frozenset({"planned", "active"}) OPEN_ACTION = frozenset({"open", "ready", "in_progress", "blocked", "review_required"}) _PRIORITY_RANK = {"critical": 0, "high": 1, "normal": 2, "low": 3} _KIND_RANK = {"bug": 0, "issue": 1, "incident": 1, "story": 2, "idea": 3} _REASON_LABELS = { "bug_critical": "Kritischer Bug — Sprint-Priorität", "bug_high_priority": "Bug mit hoher Priorität", "bug_default": "Bug — vor Features priorisieren", "issue_default": "Issue/Störung im Eingang", "story_high_priority": "Story mit hoher Priorität", "story_ready": "Story bereit zum Commit", "idea_default": "Idee im Eingang", } def _resolve_target_work_cycle( ctx: SteeringEvalContext, ) -> dict[str, Any] | None: cycles = [ c for c in ctx.work_cycles if c.get("status") in PLANABLE_CYCLE_STATUSES ] if not cycles: return None if ctx.horizon.work_cycle_id: match = next( (c for c in cycles if str(c["id"]) == str(ctx.horizon.work_cycle_id)), None, ) if match: return match planned = sorted( [c for c in cycles if c.get("status") == "planned"], key=lambda c: (c.get("sort_order", 0), str(c["id"])), ) if planned: return planned[0] active = sorted( [c for c in cycles if c.get("status") == "active"], key=lambda c: (c.get("sort_order", 0), str(c["id"])), ) return active[0] if active else None def _reason_code(item: dict[str, Any]) -> str: kind = item.get("item_kind") or "story" priority = item.get("priority") or "normal" if kind == "bug": if priority == "critical": return "bug_critical" if priority == "high": return "bug_high_priority" return "bug_default" if kind in ("issue", "incident"): return "issue_default" if kind == "idea": return "idea_default" if priority in ("critical", "high"): return "story_high_priority" return "story_ready" def _sort_key(item: dict[str, Any]) -> tuple: kind = item.get("item_kind") or "story" priority = item.get("priority") or "normal" return ( _KIND_RANK.get(kind, 9), _PRIORITY_RANK.get(priority, 9), item.get("sort_order") if item.get("sort_order") is not None else 9999, (item.get("title") or "").lower(), ) def propose_sprint_commit(ctx: SteeringEvalContext) -> list[dict[str, Any]]: """Ranked backlog items suggested for sprint commit — no auto-mutation.""" target = _resolve_target_work_cycle(ctx) if not target: return [] vocabulary = ctx.backlog_vocabulary convertible = frozenset(vocabulary.get("convertible_kinds") or ["story", "bug", "issue"]) candidates = [ item for item in ctx.backlog_items if item.get("status") in COMMITTABLE_STATUSES and item.get("item_kind") != "epic" and (item.get("item_kind") or "story") in convertible ] if not candidates: return [] ranked = sorted(candidates, key=_sort_key) cycle_id = str(target["id"]) cycle_title = target.get("title") or "Sprint" proposals: list[dict[str, Any]] = [] for rank, item in enumerate(ranked[:20], start=1): reason = _reason_code(item) kind = item.get("item_kind") or "story" proposals.append( { "proposal_key": "sprint_commit", "scope_type": "backlog_item", "scope_id": str(item["id"]), "rank": rank, "reason_code": reason, "summary": _REASON_LABELS.get(reason, "Zum Sprint committen"), "title": item.get("title") or "Backlog-Item", "item_kind": kind, "priority": item.get("priority") or "normal", "work_cycle_id": cycle_id, "work_cycle_title": cycle_title, "initiative_id": ctx.initiative_id, "confidence": "heuristic", "data_source": "steering_kernel", } ) return proposals def sprint_commit_attention_items( *, initiative_id: str, proposals: list[dict[str, Any]], actions: list[dict[str, Any]], ) -> list[dict[str, Any]]: """Attention when sprint has room for proposed commits.""" if not proposals: return [] target_id = proposals[0].get("work_cycle_id") if not target_id: return [] sprint_actions = [ a for a in actions if a.get("work_cycle_id") and str(a["work_cycle_id"]) == str(target_id) ] open_in_sprint = sum(1 for a in sprint_actions if a.get("status") in OPEN_ACTION) items: list[dict[str, Any]] = [] cycle_title = proposals[0].get("work_cycle_title") or "Sprint" if open_in_sprint == 0: items.append( { "kind": "sprint_commit_suggested", "severity": "info", "title": cycle_title, "summary": ( f"{len(proposals)} Item(s) im Eingang — " "Kernel schlägt Sprint-Commit vor" ), "scope_type": "work_cycle", "scope_id": str(target_id), "initiative_id": initiative_id, "reason_code": "sprint_commit_suggested", "data_source": "sprint_commit_proposal", } ) return items