"""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())