Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Failing after 4m15s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
K-Ext-4/P8 deklarative Agent-Slots; P7 tech_debt Read Model und Vokabular; Review-Attention in den Kernel verlagert. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""Attention contributors for reviews, recurring, tech debt."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from steering.attention.context import AttentionEvalContext
|
|
from steering.attention.contributors.registry import (
|
|
AttentionContributor,
|
|
register_attention_contributor,
|
|
)
|
|
|
|
|
|
def _parse_due(value) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
try:
|
|
text = str(value).replace("Z", "+00:00")
|
|
parsed = datetime.fromisoformat(text)
|
|
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _reviews_due(ctx: AttentionEvalContext) -> list[dict]:
|
|
if not ctx.eval_ctx or "reviews" not in ctx.eval_ctx.data_slices:
|
|
return []
|
|
now = datetime.now(timezone.utc)
|
|
items: list[dict] = []
|
|
for review in ctx.eval_ctx.reviews:
|
|
if review.get("status") != "planned":
|
|
continue
|
|
due = _parse_due(review.get("due_at"))
|
|
if due is None or due > now:
|
|
continue
|
|
items.append(
|
|
{
|
|
"kind": "review_due",
|
|
"severity": "warning",
|
|
"title": review.get("title") or "Review",
|
|
"summary": "Review fällig",
|
|
"scope_type": "review",
|
|
"scope_id": str(review["id"]),
|
|
"initiative_id": ctx.initiative_id,
|
|
"review_id": str(review["id"]),
|
|
"reason_code": "review_due",
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _recurring_due(ctx: AttentionEvalContext) -> list[dict]:
|
|
if not ctx.eval_ctx or "recurring" not in ctx.eval_ctx.data_slices:
|
|
return []
|
|
now = datetime.now(timezone.utc)
|
|
items: list[dict] = []
|
|
for element in ctx.eval_ctx.recurring_elements:
|
|
if element.get("status") != "active":
|
|
continue
|
|
due = _parse_due(element.get("next_due_at"))
|
|
if due is None or due > now:
|
|
continue
|
|
items.append(
|
|
{
|
|
"kind": "recurring_due",
|
|
"severity": "info",
|
|
"title": element.get("title") or "Rhythmus",
|
|
"summary": "Wiederkehrendes Element fällig",
|
|
"scope_type": "recurring_element",
|
|
"scope_id": str(element["id"]),
|
|
"initiative_id": ctx.initiative_id,
|
|
"recurring_element_id": str(element["id"]),
|
|
"reason_code": "recurring_due",
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _action_review_required(ctx: AttentionEvalContext) -> list[dict]:
|
|
if not ctx.eval_ctx or "reviews" not in ctx.eval_ctx.data_slices:
|
|
return []
|
|
planned_by_action = {
|
|
str(review["action_id"])
|
|
for review in ctx.eval_ctx.reviews
|
|
if review.get("status") == "planned" and review.get("action_id")
|
|
}
|
|
items: list[dict] = []
|
|
for action in ctx.actions:
|
|
if action.get("status") != "review_required":
|
|
continue
|
|
action_id = str(action["id"])
|
|
if action_id in planned_by_action:
|
|
continue
|
|
items.append(
|
|
{
|
|
"kind": "action_review_required",
|
|
"severity": "warning",
|
|
"title": action.get("title") or "Arbeitspaket",
|
|
"summary": "Maßnahme wartet auf Review — kein geplantes Review verknüpft",
|
|
"scope_type": "action",
|
|
"scope_id": action_id,
|
|
"initiative_id": ctx.initiative_id,
|
|
"action_id": action_id,
|
|
"reason_code": "action_review_required_no_review",
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _architecture_debt_stale(ctx: AttentionEvalContext) -> list[dict]:
|
|
summary = ctx.read_models.get("tech_debt_summary") or {}
|
|
if not summary.get("enabled") or not summary.get("stale_count"):
|
|
return []
|
|
return [
|
|
{
|
|
"kind": "architecture_debt_stale",
|
|
"severity": "warning",
|
|
"title": "Technische Schuld",
|
|
"summary": (
|
|
f"{summary['stale_count']} offene Schuld-Einträge älter als "
|
|
f"{summary.get('stale_days', 30)} Tage ohne Fortschritt"
|
|
),
|
|
"scope_type": "initiative",
|
|
"scope_id": ctx.initiative_id,
|
|
"initiative_id": ctx.initiative_id,
|
|
"reason_code": "architecture_debt_stale",
|
|
}
|
|
]
|
|
|
|
|
|
register_attention_contributor(
|
|
AttentionContributor(key="review_due", contribute=_reviews_due)
|
|
)
|
|
register_attention_contributor(
|
|
AttentionContributor(
|
|
key="recurring_due",
|
|
requires_elements=frozenset({"recurring_rhythm"}),
|
|
contribute=_recurring_due,
|
|
)
|
|
)
|
|
register_attention_contributor(
|
|
AttentionContributor(key="action_review_required", contribute=_action_review_required)
|
|
)
|
|
register_attention_contributor(
|
|
AttentionContributor(key="architecture_debt_stale", contribute=_architecture_debt_stale)
|
|
)
|