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
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>
179 lines
5.6 KiB
Python
179 lines
5.6 KiB
Python
"""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
|