Kairo-Jinkendo/backend/steering/proposals/sprint_commit.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

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