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
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>
158 lines
5.5 KiB
Python
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())
|