Kairo-Jinkendo/backend/steering/proposals/sprint_commit_heuristic_v0.py
Lars 1bbc5dbfce
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 4m8s
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 19s
Test Suite / playwright-smoke (push) Successful in 13s
refactor(steering): Sprint-Vorschlaege mit austauschbarem Ranker und Abhaengigkeiten
Keine feste Bug-zuerst-Policy: heuristic_v0 als Fallback, agent_v1-Slot vorbereitet, parent_action-Abhaengigkeiten und factors[] fuer spaetere KI-Steuerung.

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

158 lines
5.5 KiB
Python

"""Default sprint ranker — explicit fallback, not product policy."""
from __future__ import annotations
from typing import Any
from steering.eval_context import SteeringEvalContext
from steering.proposals.sprint_commit_context import (
RankedSprintCommitCandidate,
SprintCommitCandidate,
SprintCommitRanker,
register_sprint_commit_ranker,
)
_PRIORITY_SCORE = {"critical": 100, "high": 75, "normal": 50, "low": 25}
_KIND_BIAS = {"bug": 5, "issue": 3, "incident": 3, "story": 0, "idea": -5}
class HeuristicV0SprintCommitRanker:
"""
Fallback-Ranker bis Agent/Regelwerk aktiv ist.
Sortiert primär nach Priorität + Eingangsreihenfolge; item_kind ist nur leichte
Tendenz in `factors` — keine „Bugs immer zuerst“-Policy.
"""
key = "heuristic_v0"
def rank(
self,
ctx: SteeringEvalContext,
*,
candidates: list[SprintCommitCandidate],
read_models: dict[str, Any],
target_work_cycle_id: str,
) -> list[RankedSprintCommitCandidate]:
scored: list[tuple[float, SprintCommitCandidate, list[dict[str, Any]]]] = []
for candidate in candidates:
item = candidate.item
priority = item.get("priority") or "normal"
kind = item.get("item_kind") or "story"
priority_score = _PRIORITY_SCORE.get(priority, 50)
kind_bias = _KIND_BIAS.get(kind, 0)
factors: list[dict[str, Any]] = [
{
"code": "priority",
"weight": priority_score,
"label": f"Priorität {priority}",
},
{
"code": "intake_order",
"weight": -(item.get("sort_order") or 0),
"label": "Eingangsreihenfolge",
},
]
if kind_bias:
factors.append(
{
"code": "kind_bias",
"weight": kind_bias,
"label": f"Typ {kind} (leichte Tendenz, keine Policy)",
}
)
score = priority_score + kind_bias - (item.get("sort_order") or 0) * 0.01
if candidate.dependency_blocked:
score -= 1000
factors.append(
{
"code": "dependency_blocked",
"weight": -1000,
"label": candidate.dependency_reason or "Abhängigkeit offen",
}
)
epic_rollups = read_models.get("epic_rollup") or []
parent_epic = item.get("parent_backlog_id")
if parent_epic:
rollup = next(
(
r
for r in epic_rollups
if str(r.get("epic_backlog_id")) == str(parent_epic)
),
None,
)
if rollup and rollup.get("status") == "in_progress":
score += 2
factors.append(
{
"code": "epic_in_progress",
"weight": 2,
"label": "Epic mit laufender Arbeit",
}
)
scored.append((score, candidate, factors))
scored.sort(
key=lambda row: (
-row[0],
(row[1].item.get("title") or "").lower(),
)
)
ranked: list[RankedSprintCommitCandidate] = []
for index, (score, candidate, factors) in enumerate(scored[:20], start=1):
item = candidate.item
reason_code = _reason_code(item, candidate)
ranked.append(
RankedSprintCommitCandidate(
candidate=candidate,
rank=index,
score=score,
reason_code=reason_code,
summary=_summary_for(reason_code, candidate),
factors=factors,
ranker_key=self.key,
)
)
return ranked
def _reason_code(item: dict[str, Any], candidate: SprintCommitCandidate) -> str:
if candidate.dependency_blocked:
return candidate.dependency_reason or "dependency_blocked"
priority = item.get("priority") or "normal"
if priority == "critical":
return "priority_critical"
if priority == "high":
return "priority_high"
kind = item.get("item_kind") or "story"
if kind in ("bug", "issue", "incident"):
return "candidate_bug_or_issue"
return "candidate_ready"
def _summary_for(reason_code: str, candidate: SprintCommitCandidate) -> str:
if candidate.dependency_blocked:
if reason_code == "waiting_on_feature_action":
return "Wartet auf Feature-Arbeitspaket im Sprint oder erledigt"
if reason_code == "missing_parent_action":
return "Feature-Referenz fehlt — Abhängigkeit klären"
return "Abhängigkeit noch nicht erfüllt"
labels = {
"priority_critical": "Hohe Dringlichkeit (Priorität critical)",
"priority_high": "Erhöhte Priorität",
"candidate_bug_or_issue": "Kandidat (Heuristik — kein Bug-Zwang)",
"candidate_ready": "Bereit zum Commit (Heuristik)",
}
return labels.get(reason_code, "Sprint-Commit-Kandidat (Heuristik)")
register_sprint_commit_ranker(HeuristicV0SprintCommitRanker())