feat(steering): K-Ext-2 Attention-Registry, Gate/Intake-Proposals, generische UI
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 4m13s
Test Suite / lint-backend (push) Successful in 3s
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 12s
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 4m13s
Test Suite / lint-backend (push) Successful in 3s
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 12s
Methodenuebergreifende Provider auf Kernel v0.3; verbindliche Coding Rules fuer Agenten in ADP und .cursor/rules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1bbc5dbfce
commit
b747200f3a
|
|
@ -6,7 +6,7 @@ alwaysApply: true
|
||||||
|
|
||||||
# Kairo Plugin-Architektur (AP2.3 / AP2.4)
|
# Kairo Plugin-Architektur (AP2.3 / AP2.4)
|
||||||
|
|
||||||
Verbindliche ADPs: `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md`, `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md`, `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md`
|
Verbindliche ADPs: `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md`, `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md`, `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md`, **`docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md`**, **`.cursor/rules/kairo-steering-kernel.mdc`**
|
||||||
|
|
||||||
## Vier Schichten — Auflösung zur Laufzeit
|
## Vier Schichten — Auflösung zur Laufzeit
|
||||||
|
|
||||||
|
|
|
||||||
62
.cursor/rules/kairo-steering-kernel.mdc
Normal file
62
.cursor/rules/kairo-steering-kernel.mdc
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
description: Steering Kernel v0.3 — verbindliche Regeln für Read Models, Proposals, Attention, Ranker (alle Coding-Agenten)
|
||||||
|
globs: backend/steering/**,backend/data_layer/initiative_snapshot.py,frontend/src/components/SteeringProposalsPanel.jsx,frontend/src/components/SprintCommitProposalPanel.jsx,frontend/src/utils/steeringProposals.js,frontend/src/utils/sprintCommitProposal.js,frontend/src/pages/initiative/InitiativeInboxPage.jsx,frontend/src/pages/initiative/InitiativePlanPage.jsx,frontend/src/pages/modes/PlanSprintPage.jsx
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Kairo Steering Kernel — Coding Rules (verbindlich)
|
||||||
|
|
||||||
|
**Primärdokument:** `docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md`
|
||||||
|
**Architektur:** `docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md`
|
||||||
|
|
||||||
|
## Herzmaschine
|
||||||
|
|
||||||
|
- Initiative-Steuerung **nur** via `evaluate_steering()` in `backend/steering/kernel/evaluate.py`
|
||||||
|
- Snapshot spiegelt `evaluation.read_models` + `evaluation.proposals` — **kein** Duplikat-Rechnen
|
||||||
|
|
||||||
|
## Neue Fähigkeit = Provider registrieren
|
||||||
|
|
||||||
|
| Was | Wo |
|
||||||
|
|-----|-----|
|
||||||
|
| Read Model | `steering/read_models/registry.py` |
|
||||||
|
| Proposal | `steering/proposals/registry.py` |
|
||||||
|
| Attention | `steering/attention/contributors/*.py` + Registry |
|
||||||
|
| Sprint-Ranker | `register_sprint_commit_ranker()` — weitere Ranker analog |
|
||||||
|
|
||||||
|
Aktivierung über `steering_elements` / `data_slices` im Methoden-Vertrag — **nicht** über Page-Ifs oder `method_key`-Branches im Kernel.
|
||||||
|
|
||||||
|
## Proposals
|
||||||
|
|
||||||
|
- **Kein Auto-Commit** — Accept in UI/API
|
||||||
|
- **Keine feste Bug/Story-Policy** — `ranker_key` + `factors[]`; Heuristik = Fallback (`heuristic_v0`)
|
||||||
|
- Abhängigkeiten: `dependency_refs`, `dependency_blocked`
|
||||||
|
- KI: `agent_v1` Ranker + Actor-Slot — **keine** hardcodierten Prompts (Principle Gate)
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- `SteeringProposalsPanel` + `PROPOSAL_UI_CONFIG` in `utils/steeringProposals.js`
|
||||||
|
- Daten aus `steeringSnapshot.steering_kernel.proposals`
|
||||||
|
- Keine neue Proposal-Page pro Methode
|
||||||
|
|
||||||
|
## Methoden-Parität (Ist)
|
||||||
|
|
||||||
|
| Provider | Element-Gate |
|
||||||
|
|----------|--------------|
|
||||||
|
| `sprint_commit` | `work_cycle_scope` |
|
||||||
|
| `gate_next_actions` | `gate_fulfillment` |
|
||||||
|
| `intake_triage` | `backlog` slice, **excludes** `work_cycle_scope` |
|
||||||
|
| `epic_rollup` | `backlog_epic_hierarchy` |
|
||||||
|
| `planning_debt` / `execution_graph` | `gate_fulfillment` / `critical_path` |
|
||||||
|
|
||||||
|
## Verboten
|
||||||
|
|
||||||
|
- Steuerungslogik außerhalb `backend/steering/`
|
||||||
|
- Archetyp-Ifs für Steuerungs-UI
|
||||||
|
- `evaluate_next_work` für Commit-Vorschläge (Plan ≠ Ist)
|
||||||
|
- OM-Tabellen für Vorschläge
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Unit-Test pro Provider/Ranker
|
||||||
|
- Snapshot-Integration wenn DB-Test verfügbar
|
||||||
|
- Truth Table aktualisieren
|
||||||
|
|
@ -50,6 +50,7 @@ Lies bei Projektstart in dieser Reihenfolge:
|
||||||
8. `docs/architecture/Kairo_Tenant_Invariants_v0.1.md`
|
8. `docs/architecture/Kairo_Tenant_Invariants_v0.1.md`
|
||||||
9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md`
|
9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md`
|
||||||
10. `.cursor/rules/kairo-architecture.mdc`
|
10. `.cursor/rules/kairo-architecture.mdc`
|
||||||
|
11. **`.cursor/rules/kairo-steering-kernel.mdc`** + `docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md` (Steering Kernel v0.3 — Read Models, Proposals, Ranker)
|
||||||
11. `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md` (AP2.3 — **geliefert**)
|
11. `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v0.1.md` (AP2.3 — **geliefert**)
|
||||||
12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**)
|
12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**)
|
||||||
13. `.cursor/rules/kairo-plugin-architecture.mdc`
|
13. `.cursor/rules/kairo-plugin-architecture.mdc`
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,8 @@ def get_initiative_steering_snapshot(
|
||||||
"active_work_cycle": active_work_cycle,
|
"active_work_cycle": active_work_cycle,
|
||||||
"epic_rollup": epic_rollup,
|
"epic_rollup": epic_rollup,
|
||||||
"sprint_commit_proposals": sprint_commit_proposals,
|
"sprint_commit_proposals": sprint_commit_proposals,
|
||||||
|
"gate_next_actions_proposals": evaluation.proposals.get("gate_next_actions") or [],
|
||||||
|
"intake_triage_proposals": evaluation.proposals.get("intake_triage") or [],
|
||||||
"counts": {
|
"counts": {
|
||||||
"actions_open": open_actions,
|
"actions_open": open_actions,
|
||||||
"actions_blocked": blocked_actions,
|
"actions_blocked": blocked_actions,
|
||||||
|
|
|
||||||
1
backend/steering/attention/__init__.py
Normal file
1
backend/steering/attention/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Attention contributors — K-Ext-2."""
|
||||||
109
backend/steering/attention/context.py
Normal file
109
backend/steering/attention/context.py
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
"""Attention evaluation context — shared caches for contributors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
from steering.kernel.models import HorizonMarker, SteeringBinding
|
||||||
|
from tenant_context import TenantContext
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AttentionEvalContext:
|
||||||
|
tenant_ctx: TenantContext
|
||||||
|
initiative_id: str
|
||||||
|
binding: SteeringBinding
|
||||||
|
horizon: HorizonMarker
|
||||||
|
next_work: list[dict[str, Any]]
|
||||||
|
read_models: dict[str, Any]
|
||||||
|
proposals: dict[str, Any]
|
||||||
|
eval_ctx: SteeringEvalContext | None = None
|
||||||
|
|
||||||
|
_actions: list[dict[str, Any]] | None = field(default=None, repr=False)
|
||||||
|
_blockers: list[dict[str, Any]] | None = field(default=None, repr=False)
|
||||||
|
_roadmap_items: list[dict[str, Any]] | None = field(default=None, repr=False)
|
||||||
|
_dependencies: list[dict[str, Any]] | None = field(default=None, repr=False)
|
||||||
|
_graph_state: dict[str, Any] | None = field(default=None, repr=False)
|
||||||
|
_gate_scope: str | None = field(default=None, repr=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def steering_elements(self) -> frozenset[str]:
|
||||||
|
if self.eval_ctx:
|
||||||
|
return self.eval_ctx.steering_elements
|
||||||
|
return frozenset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actions(self) -> list[dict[str, Any]]:
|
||||||
|
if self._actions is None:
|
||||||
|
if self.eval_ctx:
|
||||||
|
self._actions = self.eval_ctx.actions
|
||||||
|
else:
|
||||||
|
from services import actions as action_service
|
||||||
|
|
||||||
|
self._actions = action_service.list_actions_for_initiative(
|
||||||
|
tenant_id=self.tenant_ctx.tenant_id,
|
||||||
|
initiative_id=self.initiative_id,
|
||||||
|
)
|
||||||
|
return self._actions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blockers(self) -> list[dict[str, Any]]:
|
||||||
|
if self._blockers is None:
|
||||||
|
from services import blockers as blocker_service
|
||||||
|
|
||||||
|
self._blockers = blocker_service.list_blockers_for_initiative(
|
||||||
|
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
|
||||||
|
)
|
||||||
|
return self._blockers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def roadmap_items(self) -> list[dict[str, Any]]:
|
||||||
|
if self._roadmap_items is None:
|
||||||
|
from services import roadmap as roadmap_service
|
||||||
|
|
||||||
|
self._roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
|
||||||
|
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
|
||||||
|
)
|
||||||
|
return self._roadmap_items
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gate_scope(self) -> str | None:
|
||||||
|
if self._gate_scope is None:
|
||||||
|
from steering.strategies.next_action.execution_ready import (
|
||||||
|
gate_horizon_scope_for_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._gate_scope = gate_horizon_scope_for_method(
|
||||||
|
self.binding.primary_method_key,
|
||||||
|
self.tenant_ctx,
|
||||||
|
self.initiative_id,
|
||||||
|
)
|
||||||
|
return self._gate_scope
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dependencies(self) -> list[dict[str, Any]]:
|
||||||
|
if self._dependencies is None:
|
||||||
|
from services.execution_plan import list_dependencies_for_initiative
|
||||||
|
|
||||||
|
self._dependencies = list_dependencies_for_initiative(
|
||||||
|
tenant_id=self.tenant_ctx.tenant_id, initiative_id=self.initiative_id
|
||||||
|
)
|
||||||
|
return self._dependencies
|
||||||
|
|
||||||
|
@property
|
||||||
|
def graph_state(self) -> dict[str, Any]:
|
||||||
|
if self._graph_state is None:
|
||||||
|
cached = self.read_models.get("execution_graph")
|
||||||
|
if cached is not None:
|
||||||
|
self._graph_state = cached
|
||||||
|
else:
|
||||||
|
from steering.graph.execution_engine import compute_execution_graph_state
|
||||||
|
|
||||||
|
self._graph_state = compute_execution_graph_state(
|
||||||
|
actions=self.actions,
|
||||||
|
dependencies=self.dependencies,
|
||||||
|
scope_roadmap_item_id=self.gate_scope,
|
||||||
|
)
|
||||||
|
return self._graph_state
|
||||||
1
backend/steering/attention/contributors/__init__.py
Normal file
1
backend/steering/attention/contributors/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Attention contributors package."""
|
||||||
62
backend/steering/attention/contributors/core.py
Normal file
62
backend/steering/attention/contributors/core.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Core attention contributors — blockers, blocked actions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from steering.attention.context import AttentionEvalContext
|
||||||
|
from steering.attention.contributors.registry import (
|
||||||
|
AttentionContributor,
|
||||||
|
register_attention_contributor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_blockers(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
items: list[dict] = []
|
||||||
|
for blocker in ctx.blockers:
|
||||||
|
if blocker.get("status") not in ("open", "in_progress"):
|
||||||
|
continue
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"kind": "open_blocker",
|
||||||
|
"severity": "warning",
|
||||||
|
"title": blocker.get("title") or "Blocker",
|
||||||
|
"summary": "Offener Blocker im Vorhaben",
|
||||||
|
"scope_type": "blocker",
|
||||||
|
"scope_id": str(blocker["id"]),
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"action_id": (
|
||||||
|
str(blocker["action_id"]) if blocker.get("action_id") else None
|
||||||
|
),
|
||||||
|
"blocker_id": str(blocker["id"]),
|
||||||
|
"reason_code": "path_blocked",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_actions(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
items: list[dict] = []
|
||||||
|
for action in ctx.actions:
|
||||||
|
if action.get("status") != "blocked":
|
||||||
|
continue
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"kind": "blocked_action",
|
||||||
|
"severity": "critical",
|
||||||
|
"title": action.get("title") or "Arbeitspaket",
|
||||||
|
"summary": "Maßnahme ist blockiert",
|
||||||
|
"scope_type": "action",
|
||||||
|
"scope_id": str(action["id"]),
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"action_id": str(action["id"]),
|
||||||
|
"reason_code": "path_blocked",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(key="open_blockers", contribute=_open_blockers)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(key="blocked_actions", contribute=_blocked_actions)
|
||||||
|
)
|
||||||
105
backend/steering/attention/contributors/gates.py
Normal file
105
backend/steering/attention/contributors/gates.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
"""Gate / execution graph attention — planning_debt, execution_waiting."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from steering.attention.context import AttentionEvalContext
|
||||||
|
from steering.attention.contributors.registry import (
|
||||||
|
AttentionContributor,
|
||||||
|
register_attention_contributor,
|
||||||
|
)
|
||||||
|
from steering.graph.execution_engine import (
|
||||||
|
execution_waiting_to_attention_items,
|
||||||
|
planning_debt_to_attention_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
_GATE_METHODS = frozenset(
|
||||||
|
{"sequential_dependency", "program_delivery", "maturity_progression"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _planning_debt(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
if ctx.binding.primary_method_key not in _GATE_METHODS:
|
||||||
|
return []
|
||||||
|
debts = ctx.read_models.get("planning_debt")
|
||||||
|
if debts is None:
|
||||||
|
from steering.graph.execution_engine import compute_planning_debt
|
||||||
|
|
||||||
|
gate_scope = ctx.gate_scope
|
||||||
|
scoped_roadmap = ctx.roadmap_items
|
||||||
|
scoped_actions = ctx.actions
|
||||||
|
if gate_scope:
|
||||||
|
scoped_roadmap = [
|
||||||
|
ri for ri in ctx.roadmap_items if str(ri.get("id")) == str(gate_scope)
|
||||||
|
]
|
||||||
|
scoped_actions = [
|
||||||
|
a
|
||||||
|
for a in ctx.actions
|
||||||
|
if a.get("roadmap_item_id")
|
||||||
|
and str(a["roadmap_item_id"]) == str(gate_scope)
|
||||||
|
]
|
||||||
|
debts = compute_planning_debt(
|
||||||
|
actions=scoped_actions, roadmap_items=scoped_roadmap
|
||||||
|
)
|
||||||
|
return planning_debt_to_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id, debts=debts or []
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_waiting(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
if "critical_path" not in ctx.steering_elements and "gate_fulfillment" not in ctx.steering_elements:
|
||||||
|
return []
|
||||||
|
return execution_waiting_to_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id,
|
||||||
|
actions=ctx.actions,
|
||||||
|
graph_state=ctx.graph_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _horizon_no_ready_work(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
if ctx.next_work:
|
||||||
|
return []
|
||||||
|
if ctx.binding.primary_method_key not in _GATE_METHODS:
|
||||||
|
return []
|
||||||
|
if ctx.horizon.kind != "gate":
|
||||||
|
return []
|
||||||
|
|
||||||
|
debts = ctx.read_models.get("planning_debt") or []
|
||||||
|
has_planning = len(debts) > 0
|
||||||
|
has_waiting = bool(ctx.graph_state.get("blocked_actions"))
|
||||||
|
if not has_planning and not has_waiting and not ctx.graph_state.get("ready_actions"):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"kind": "initiative_without_next_action",
|
||||||
|
"severity": "info",
|
||||||
|
"title": ctx.horizon.gate_title or "Aktiver Horizont",
|
||||||
|
"summary": "Keine ausführungsbereite Arbeit im aktiven Horizont",
|
||||||
|
"scope_type": "milestone",
|
||||||
|
"scope_id": ctx.horizon.gate_roadmap_item_id,
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"reason_code": "horizon_no_ready_work",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="planning_debt",
|
||||||
|
requires_elements=frozenset({"gate_fulfillment"}),
|
||||||
|
contribute=_planning_debt,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="execution_waiting",
|
||||||
|
requires_any_element=frozenset({"critical_path", "gate_fulfillment"}),
|
||||||
|
contribute=_execution_waiting,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="horizon_no_ready_work",
|
||||||
|
requires_elements=frozenset({"gate_fulfillment"}),
|
||||||
|
contribute=_horizon_no_ready_work,
|
||||||
|
)
|
||||||
|
)
|
||||||
80
backend/steering/attention/contributors/read_models.py
Normal file
80
backend/steering/attention/contributors/read_models.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""Read-model / proposal derived attention."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from steering.attention.context import AttentionEvalContext
|
||||||
|
from steering.attention.contributors.registry import (
|
||||||
|
AttentionContributor,
|
||||||
|
register_attention_contributor,
|
||||||
|
)
|
||||||
|
from steering.proposals.gate_next_actions import gate_next_actions_attention_items
|
||||||
|
from steering.proposals.intake_triage import intake_triage_attention_items
|
||||||
|
from steering.proposals.sprint_commit import sprint_commit_attention_items
|
||||||
|
from steering.read_models.epic_rollup import epic_rollup_attention_items
|
||||||
|
|
||||||
|
|
||||||
|
def _epic_rollup(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
rollups = ctx.read_models.get("epic_rollup")
|
||||||
|
if not rollups:
|
||||||
|
return []
|
||||||
|
return epic_rollup_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id, rollups=rollups
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sprint_commit(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
proposals = ctx.proposals.get("sprint_commit")
|
||||||
|
if not proposals:
|
||||||
|
return []
|
||||||
|
return sprint_commit_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id,
|
||||||
|
proposals=proposals,
|
||||||
|
actions=ctx.actions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_next_actions(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
proposals = ctx.proposals.get("gate_next_actions")
|
||||||
|
if not proposals:
|
||||||
|
return []
|
||||||
|
return gate_next_actions_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id, proposals=proposals
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _intake_triage(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
proposals = ctx.proposals.get("intake_triage")
|
||||||
|
if not proposals:
|
||||||
|
return []
|
||||||
|
return intake_triage_attention_items(
|
||||||
|
initiative_id=ctx.initiative_id, proposals=proposals
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="epic_rollup",
|
||||||
|
requires_elements=frozenset({"backlog_epic_hierarchy"}),
|
||||||
|
contribute=_epic_rollup,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="sprint_commit",
|
||||||
|
requires_elements=frozenset({"work_cycle_scope"}),
|
||||||
|
contribute=_sprint_commit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="gate_next_actions",
|
||||||
|
requires_elements=frozenset({"gate_fulfillment"}),
|
||||||
|
contribute=_gate_next_actions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_attention_contributor(
|
||||||
|
AttentionContributor(
|
||||||
|
key="intake_triage",
|
||||||
|
contribute=_intake_triage,
|
||||||
|
)
|
||||||
|
)
|
||||||
59
backend/steering/attention/contributors/registry.py
Normal file
59
backend/steering/attention/contributors/registry.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""Attention contributor registry — K-Ext-2."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from steering.attention.context import AttentionEvalContext
|
||||||
|
|
||||||
|
AttentionContribute = Callable[[AttentionEvalContext], list[dict]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AttentionContributor:
|
||||||
|
key: str
|
||||||
|
requires_elements: frozenset[str] = field(default_factory=frozenset)
|
||||||
|
requires_any_element: frozenset[str] = field(default_factory=frozenset)
|
||||||
|
requires_primary_methods: frozenset[str] = field(default_factory=frozenset)
|
||||||
|
contribute: AttentionContribute = lambda _ctx: []
|
||||||
|
|
||||||
|
|
||||||
|
_CONTRIBUTORS: list[AttentionContributor] = []
|
||||||
|
|
||||||
|
|
||||||
|
def register_attention_contributor(contributor: AttentionContributor) -> None:
|
||||||
|
_CONTRIBUTORS.append(contributor)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_attention_items(ctx: AttentionEvalContext) -> list[dict]:
|
||||||
|
elements = ctx.steering_elements
|
||||||
|
primary = ctx.binding.primary_method_key
|
||||||
|
items: list[dict] = []
|
||||||
|
|
||||||
|
for contributor in _CONTRIBUTORS:
|
||||||
|
if (
|
||||||
|
contributor.requires_elements
|
||||||
|
and not contributor.requires_elements <= elements
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if contributor.requires_any_element and not (
|
||||||
|
contributor.requires_any_element & elements
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
contributor.requires_primary_methods
|
||||||
|
and primary not in contributor.requires_primary_methods
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
items.extend(contributor.contribute(ctx))
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item.setdefault("data_source", "steering_kernel")
|
||||||
|
|
||||||
|
items.sort(
|
||||||
|
key=lambda x: {"critical": 0, "warning": 1, "info": 2}.get(
|
||||||
|
x.get("severity", "info"), 9
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
@ -5,16 +5,12 @@ from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from steering.kernel.models import HorizonMarker, SteeringBinding
|
from steering.kernel.models import HorizonMarker, SteeringBinding
|
||||||
from steering.strategies.next_action.execution_ready import gate_horizon_scope_for_method
|
|
||||||
from tenant_context import TenantContext
|
from tenant_context import TenantContext
|
||||||
|
|
||||||
_GATE_METHODS = frozenset(
|
# Side-effect: register contributors
|
||||||
{
|
import steering.attention.contributors.core # noqa: F401
|
||||||
"sequential_dependency",
|
import steering.attention.contributors.gates # noqa: F401
|
||||||
"program_delivery",
|
import steering.attention.contributors.read_models # noqa: F401
|
||||||
"maturity_progression",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_attention(
|
def evaluate_attention(
|
||||||
|
|
@ -29,161 +25,18 @@ def evaluate_attention(
|
||||||
eval_ctx=None,
|
eval_ctx=None,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Kernel attention for a single initiative — no fake next when planning debt."""
|
"""Kernel attention — contributors via registry (K-Ext-2)."""
|
||||||
from services import actions as action_service
|
from steering.attention.context import AttentionEvalContext
|
||||||
from services import blockers as blocker_service
|
from steering.attention.contributors.registry import collect_attention_items
|
||||||
from services import roadmap as roadmap_service
|
|
||||||
from services.execution_plan import list_dependencies_for_initiative
|
actx = AttentionEvalContext(
|
||||||
from steering.graph.execution_engine import (
|
tenant_ctx=ctx,
|
||||||
compute_execution_graph_state,
|
initiative_id=initiative_id,
|
||||||
compute_planning_debt,
|
binding=binding,
|
||||||
execution_waiting_to_attention_items,
|
horizon=horizon,
|
||||||
planning_debt_to_attention_items,
|
next_work=next_work,
|
||||||
|
read_models=read_models or {},
|
||||||
|
proposals=proposals or {},
|
||||||
|
eval_ctx=eval_ctx,
|
||||||
)
|
)
|
||||||
|
return collect_attention_items(actx)[:limit]
|
||||||
items: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
blockers = blocker_service.list_blockers_for_initiative(
|
|
||||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
|
||||||
)
|
|
||||||
for blocker in blockers:
|
|
||||||
if blocker.get("status") not in ("open", "in_progress"):
|
|
||||||
continue
|
|
||||||
items.append(
|
|
||||||
{
|
|
||||||
"kind": "open_blocker",
|
|
||||||
"severity": "warning",
|
|
||||||
"title": blocker.get("title") or "Blocker",
|
|
||||||
"summary": "Offener Blocker im Vorhaben",
|
|
||||||
"scope_type": "blocker",
|
|
||||||
"scope_id": str(blocker["id"]),
|
|
||||||
"initiative_id": initiative_id,
|
|
||||||
"action_id": (
|
|
||||||
str(blocker["action_id"]) if blocker.get("action_id") else None
|
|
||||||
),
|
|
||||||
"blocker_id": str(blocker["id"]),
|
|
||||||
"reason_code": "path_blocked",
|
|
||||||
"data_source": "steering_kernel",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
actions = action_service.list_actions_for_initiative(
|
|
||||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
|
||||||
)
|
|
||||||
for action in actions:
|
|
||||||
if action.get("status") != "blocked":
|
|
||||||
continue
|
|
||||||
items.append(
|
|
||||||
{
|
|
||||||
"kind": "blocked_action",
|
|
||||||
"severity": "critical",
|
|
||||||
"title": action.get("title") or "Arbeitspaket",
|
|
||||||
"summary": "Maßnahme ist blockiert",
|
|
||||||
"scope_type": "action",
|
|
||||||
"scope_id": str(action["id"]),
|
|
||||||
"initiative_id": initiative_id,
|
|
||||||
"action_id": str(action["id"]),
|
|
||||||
"reason_code": "path_blocked",
|
|
||||||
"data_source": "steering_kernel",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
gate_scope = gate_horizon_scope_for_method(
|
|
||||||
binding.primary_method_key, ctx, initiative_id
|
|
||||||
)
|
|
||||||
roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
|
|
||||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
|
||||||
)
|
|
||||||
|
|
||||||
scoped_roadmap = roadmap_items
|
|
||||||
scoped_actions = actions
|
|
||||||
if gate_scope:
|
|
||||||
scope = str(gate_scope)
|
|
||||||
scoped_roadmap = [ri for ri in roadmap_items if str(ri.get("id")) == scope]
|
|
||||||
scoped_actions = [
|
|
||||||
a
|
|
||||||
for a in actions
|
|
||||||
if a.get("roadmap_item_id") and str(a["roadmap_item_id"]) == scope
|
|
||||||
]
|
|
||||||
|
|
||||||
if binding.primary_method_key in _GATE_METHODS:
|
|
||||||
debts = compute_planning_debt(
|
|
||||||
actions=scoped_actions, roadmap_items=scoped_roadmap
|
|
||||||
)
|
|
||||||
items.extend(
|
|
||||||
planning_debt_to_attention_items(
|
|
||||||
initiative_id=initiative_id, debts=debts
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dependencies = list_dependencies_for_initiative(
|
|
||||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
|
||||||
)
|
|
||||||
graph_state = compute_execution_graph_state(
|
|
||||||
actions=actions,
|
|
||||||
dependencies=dependencies,
|
|
||||||
scope_roadmap_item_id=gate_scope,
|
|
||||||
)
|
|
||||||
items.extend(
|
|
||||||
execution_waiting_to_attention_items(
|
|
||||||
initiative_id=initiative_id,
|
|
||||||
actions=actions,
|
|
||||||
graph_state=graph_state,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
read_models = read_models or {}
|
|
||||||
proposals = proposals or {}
|
|
||||||
|
|
||||||
from steering.read_models.epic_rollup import epic_rollup_attention_items
|
|
||||||
from steering.proposals.sprint_commit import sprint_commit_attention_items
|
|
||||||
|
|
||||||
epic_rollups = read_models.get("epic_rollup")
|
|
||||||
if epic_rollups:
|
|
||||||
items.extend(
|
|
||||||
epic_rollup_attention_items(
|
|
||||||
initiative_id=initiative_id, rollups=epic_rollups
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
sprint_proposals = proposals.get("sprint_commit")
|
|
||||||
if sprint_proposals:
|
|
||||||
items.extend(
|
|
||||||
sprint_commit_attention_items(
|
|
||||||
initiative_id=initiative_id,
|
|
||||||
proposals=sprint_proposals,
|
|
||||||
actions=actions,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not next_work
|
|
||||||
and binding.primary_method_key in _GATE_METHODS
|
|
||||||
and horizon.kind == "gate"
|
|
||||||
):
|
|
||||||
has_planning = any(i.get("reason_code") == "planning_debt" for i in items)
|
|
||||||
has_waiting = any(i.get("reason_code") == "execution_waiting" for i in items)
|
|
||||||
if not has_planning and not has_waiting and not graph_state.get("ready_actions"):
|
|
||||||
items.append(
|
|
||||||
{
|
|
||||||
"kind": "initiative_without_next_action",
|
|
||||||
"severity": "info",
|
|
||||||
"title": horizon.gate_title or "Aktiver Horizont",
|
|
||||||
"summary": "Keine ausführungsbereite Arbeit im aktiven Horizont",
|
|
||||||
"scope_type": "milestone",
|
|
||||||
"scope_id": horizon.gate_roadmap_item_id,
|
|
||||||
"initiative_id": initiative_id,
|
|
||||||
"reason_code": "horizon_no_ready_work",
|
|
||||||
"data_source": "steering_kernel",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
item.setdefault("data_source", "steering_kernel")
|
|
||||||
|
|
||||||
items.sort(
|
|
||||||
key=lambda x: {"critical": 0, "warning": 1, "info": 2}.get(
|
|
||||||
x.get("severity", "info"), 9
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return items[:limit]
|
|
||||||
|
|
|
||||||
127
backend/steering/proposals/gate_next_actions.py
Normal file
127
backend/steering/proposals/gate_next_actions.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
"""Gate next actions proposal — sequential_dependency / program_delivery."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
|
||||||
|
COMMITTABLE = frozenset({"new", "triaged", "accepted"})
|
||||||
|
_PRIORITY = {"critical": 0, "high": 1, "normal": 2, "low": 3}
|
||||||
|
|
||||||
|
|
||||||
|
def _active_gate_id(ctx: SteeringEvalContext) -> str | None:
|
||||||
|
if ctx.horizon.kind == "gate" and ctx.horizon.gate_roadmap_item_id:
|
||||||
|
return str(ctx.horizon.gate_roadmap_item_id)
|
||||||
|
debts = []
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_key(item: dict[str, Any]) -> tuple:
|
||||||
|
return (
|
||||||
|
_PRIORITY.get(item.get("priority") or "normal", 9),
|
||||||
|
item.get("sort_order") if item.get("sort_order") is not None else 9999,
|
||||||
|
(item.get("title") or "").lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def propose_gate_next_actions(
|
||||||
|
ctx: SteeringEvalContext,
|
||||||
|
*,
|
||||||
|
read_models: dict[str, Any] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
read_models = read_models or {}
|
||||||
|
gate_id = _active_gate_id(ctx)
|
||||||
|
proposals: list[dict[str, Any]] = []
|
||||||
|
rank = 0
|
||||||
|
|
||||||
|
for debt in read_models.get("planning_debt") or []:
|
||||||
|
rank += 1
|
||||||
|
gid = str(debt.get("roadmap_item_id"))
|
||||||
|
proposals.append(
|
||||||
|
{
|
||||||
|
"proposal_key": "gate_next_actions",
|
||||||
|
"scope_type": "roadmap_item",
|
||||||
|
"scope_id": gid,
|
||||||
|
"rank": rank,
|
||||||
|
"reason_code": "planning_debt",
|
||||||
|
"summary": debt.get("message") or "Gate ohne Durchführungsplan",
|
||||||
|
"title": debt.get("title") or "Zielzustand",
|
||||||
|
"roadmap_item_id": gid,
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"ranker_key": "heuristic_v0",
|
||||||
|
"confidence": "heuristic",
|
||||||
|
"factors": [{"code": "planning_debt", "weight": 100, "label": "Planning Debt"}],
|
||||||
|
"dependency_refs": [],
|
||||||
|
"dependency_blocked": False,
|
||||||
|
"data_source": "steering_kernel",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
and item.get("item_kind") != "epic"
|
||||||
|
and (item.get("item_kind") or "story") in convertible
|
||||||
|
and (not gate_id or str(item.get("roadmap_item_id") or "") == gate_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in sorted(candidates, key=_sort_key)[:15]:
|
||||||
|
rank += 1
|
||||||
|
proposals.append(
|
||||||
|
{
|
||||||
|
"proposal_key": "gate_next_actions",
|
||||||
|
"scope_type": "backlog_item",
|
||||||
|
"scope_id": str(item["id"]),
|
||||||
|
"rank": rank,
|
||||||
|
"reason_code": "gate_backlog_ready",
|
||||||
|
"summary": "Eingang-Item am aktiven Gate committen",
|
||||||
|
"title": item.get("title") or "Backlog-Item",
|
||||||
|
"item_kind": item.get("item_kind") or "story",
|
||||||
|
"priority": item.get("priority") or "normal",
|
||||||
|
"roadmap_item_id": (
|
||||||
|
str(item["roadmap_item_id"]) if item.get("roadmap_item_id") else gate_id
|
||||||
|
),
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"ranker_key": "heuristic_v0",
|
||||||
|
"confidence": "heuristic",
|
||||||
|
"factors": [
|
||||||
|
{
|
||||||
|
"code": "gate_alignment",
|
||||||
|
"weight": 50,
|
||||||
|
"label": "Gate-Zuordnung",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependency_refs": [],
|
||||||
|
"dependency_blocked": False,
|
||||||
|
"data_source": "steering_kernel",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
|
def gate_next_actions_attention_items(
|
||||||
|
*,
|
||||||
|
initiative_id: str,
|
||||||
|
proposals: list[dict[str, Any]],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
backlog_ready = [p for p in proposals if p.get("scope_type") == "backlog_item"]
|
||||||
|
if not backlog_ready:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"kind": "gate_commit_suggested",
|
||||||
|
"severity": "info",
|
||||||
|
"title": "Gate-Planung",
|
||||||
|
"summary": f"{len(backlog_ready)} Item(s) für Gate-Commit vorgeschlagen",
|
||||||
|
"scope_type": "initiative",
|
||||||
|
"scope_id": initiative_id,
|
||||||
|
"initiative_id": initiative_id,
|
||||||
|
"reason_code": "gate_commit_suggested",
|
||||||
|
"data_source": "gate_next_actions",
|
||||||
|
}
|
||||||
|
]
|
||||||
93
backend/steering/proposals/intake_triage.py
Normal file
93
backend/steering/proposals/intake_triage.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Intake triage proposal — queue_inbox / continuous backlog (no sprint)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
|
||||||
|
COMMITTABLE = frozenset({"new", "triaged", "accepted"})
|
||||||
|
_PRIORITY = {"critical": 0, "high": 1, "normal": 2, "low": 3}
|
||||||
|
_QUEUE_KIND_BIAS = {"bug": 0, "issue": 1, "incident": 1, "story": 2, "idea": 3}
|
||||||
|
|
||||||
|
|
||||||
|
def propose_intake_triage(
|
||||||
|
ctx: SteeringEvalContext,
|
||||||
|
*,
|
||||||
|
read_models: dict[str, Any] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
vocabulary = ctx.backlog_vocabulary
|
||||||
|
convertible = frozenset(vocabulary.get("convertible_kinds") or ["story", "bug", "issue"])
|
||||||
|
queue_mode = "queue_inbox" in ctx.steering_elements
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
item
|
||||||
|
for item in ctx.backlog_items
|
||||||
|
if item.get("status") in COMMITTABLE
|
||||||
|
and item.get("item_kind") != "epic"
|
||||||
|
and (item.get("item_kind") or "story") in convertible
|
||||||
|
]
|
||||||
|
if not candidates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def sort_key(item: dict[str, Any]) -> tuple:
|
||||||
|
kind = item.get("item_kind") or "story"
|
||||||
|
kind_bias = _QUEUE_KIND_BIAS.get(kind, 5) if queue_mode else 5
|
||||||
|
return (
|
||||||
|
_PRIORITY.get(item.get("priority") or "normal", 9),
|
||||||
|
kind_bias,
|
||||||
|
item.get("sort_order") if item.get("sort_order") is not None else 9999,
|
||||||
|
)
|
||||||
|
|
||||||
|
proposals: list[dict[str, Any]] = []
|
||||||
|
for rank, item in enumerate(sorted(candidates, key=sort_key)[:20], start=1):
|
||||||
|
kind = item.get("item_kind") or "story"
|
||||||
|
proposals.append(
|
||||||
|
{
|
||||||
|
"proposal_key": "intake_triage",
|
||||||
|
"scope_type": "backlog_item",
|
||||||
|
"scope_id": str(item["id"]),
|
||||||
|
"rank": rank,
|
||||||
|
"reason_code": "intake_triage_candidate",
|
||||||
|
"summary": "Triage-Kandidat (Heuristik — Ranker austauschbar)",
|
||||||
|
"title": item.get("title") or "Backlog-Item",
|
||||||
|
"item_kind": kind,
|
||||||
|
"priority": item.get("priority") or "normal",
|
||||||
|
"initiative_id": ctx.initiative_id,
|
||||||
|
"ranker_key": "heuristic_v0",
|
||||||
|
"confidence": "heuristic",
|
||||||
|
"factors": [
|
||||||
|
{
|
||||||
|
"code": "priority",
|
||||||
|
"weight": _PRIORITY.get(item.get("priority") or "normal", 9),
|
||||||
|
"label": "Priorität",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependency_refs": [],
|
||||||
|
"dependency_blocked": False,
|
||||||
|
"data_source": "steering_kernel",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
|
def intake_triage_attention_items(
|
||||||
|
*,
|
||||||
|
initiative_id: str,
|
||||||
|
proposals: list[dict[str, Any]],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if not proposals:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"kind": "intake_triage_suggested",
|
||||||
|
"severity": "info",
|
||||||
|
"title": "Eingang triagieren",
|
||||||
|
"summary": f"{len(proposals)} Item(s) — Kernel schlägt Triage-Reihenfolge vor",
|
||||||
|
"scope_type": "initiative",
|
||||||
|
"scope_id": initiative_id,
|
||||||
|
"initiative_id": initiative_id,
|
||||||
|
"reason_code": "intake_triage_suggested",
|
||||||
|
"data_source": "intake_triage",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from steering.eval_context import SteeringEvalContext
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
|
@ -16,6 +16,7 @@ class ProposalProvider:
|
||||||
requires_elements: frozenset[str]
|
requires_elements: frozenset[str]
|
||||||
requires_data_slices: frozenset[str]
|
requires_data_slices: frozenset[str]
|
||||||
compute: ProposalCompute
|
compute: ProposalCompute
|
||||||
|
excludes_elements: frozenset[str] = field(default_factory=frozenset)
|
||||||
|
|
||||||
|
|
||||||
_PROVIDERS: list[ProposalProvider] = []
|
_PROVIDERS: list[ProposalProvider] = []
|
||||||
|
|
@ -37,6 +38,8 @@ def compute_proposals(
|
||||||
for provider in _PROVIDERS:
|
for provider in _PROVIDERS:
|
||||||
if provider.requires_elements and not provider.requires_elements <= elements:
|
if provider.requires_elements and not provider.requires_elements <= elements:
|
||||||
continue
|
continue
|
||||||
|
if provider.excludes_elements and provider.excludes_elements & elements:
|
||||||
|
continue
|
||||||
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
||||||
continue
|
continue
|
||||||
result[provider.key] = provider.compute(ctx, read_models)
|
result[provider.key] = provider.compute(ctx, read_models)
|
||||||
|
|
@ -45,6 +48,8 @@ def compute_proposals(
|
||||||
|
|
||||||
|
|
||||||
def _register_builtin_proposals() -> None:
|
def _register_builtin_proposals() -> None:
|
||||||
|
from steering.proposals.gate_next_actions import propose_gate_next_actions
|
||||||
|
from steering.proposals.intake_triage import propose_intake_triage
|
||||||
from steering.proposals.sprint_commit import propose_sprint_commit
|
from steering.proposals.sprint_commit import propose_sprint_commit
|
||||||
|
|
||||||
register_proposal(
|
register_proposal(
|
||||||
|
|
@ -52,7 +57,30 @@ def _register_builtin_proposals() -> None:
|
||||||
key="sprint_commit",
|
key="sprint_commit",
|
||||||
requires_elements=frozenset({"work_cycle_scope"}),
|
requires_elements=frozenset({"work_cycle_scope"}),
|
||||||
requires_data_slices=frozenset({"backlog", "work_cycles"}),
|
requires_data_slices=frozenset({"backlog", "work_cycles"}),
|
||||||
compute=lambda ctx, read_models: propose_sprint_commit(ctx, read_models=read_models),
|
compute=lambda ctx, read_models: propose_sprint_commit(
|
||||||
|
ctx, read_models=read_models
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_proposal(
|
||||||
|
ProposalProvider(
|
||||||
|
key="gate_next_actions",
|
||||||
|
requires_elements=frozenset({"gate_fulfillment"}),
|
||||||
|
requires_data_slices=frozenset({"backlog", "roadmap", "actions"}),
|
||||||
|
compute=lambda ctx, read_models: propose_gate_next_actions(
|
||||||
|
ctx, read_models=read_models
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_proposal(
|
||||||
|
ProposalProvider(
|
||||||
|
key="intake_triage",
|
||||||
|
requires_elements=frozenset(),
|
||||||
|
requires_data_slices=frozenset({"backlog"}),
|
||||||
|
excludes_elements=frozenset({"work_cycle_scope"}),
|
||||||
|
compute=lambda ctx, read_models: propose_intake_triage(
|
||||||
|
ctx, read_models=read_models
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
27
backend/steering/read_models/execution_graph.py
Normal file
27
backend/steering/read_models/execution_graph.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Execution graph read model — critical_path / gate_fulfillment."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
from steering.graph.execution_engine import compute_execution_graph_state
|
||||||
|
|
||||||
|
|
||||||
|
def compute_execution_graph_read_model(ctx: SteeringEvalContext) -> dict[str, Any]:
|
||||||
|
from services.execution_plan import list_dependencies_for_initiative
|
||||||
|
from steering.strategies.next_action.execution_ready import (
|
||||||
|
gate_horizon_scope_for_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
gate_scope = gate_horizon_scope_for_method(
|
||||||
|
ctx.binding.primary_method_key, ctx.tenant_ctx, ctx.initiative_id
|
||||||
|
)
|
||||||
|
dependencies = list_dependencies_for_initiative(
|
||||||
|
tenant_id=ctx.tenant_ctx.tenant_id, initiative_id=ctx.initiative_id
|
||||||
|
)
|
||||||
|
return compute_execution_graph_state(
|
||||||
|
actions=ctx.actions,
|
||||||
|
dependencies=dependencies,
|
||||||
|
scope_roadmap_item_id=gate_scope,
|
||||||
|
)
|
||||||
36
backend/steering/read_models/planning_debt.py
Normal file
36
backend/steering/read_models/planning_debt.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Planning debt read model — gate_fulfillment."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
from steering.graph.execution_engine import compute_planning_debt
|
||||||
|
|
||||||
|
|
||||||
|
def compute_planning_debt_read_model(ctx: SteeringEvalContext) -> list[dict[str, Any]]:
|
||||||
|
from services import roadmap as roadmap_service
|
||||||
|
from steering.strategies.next_action.execution_ready import (
|
||||||
|
gate_horizon_scope_for_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
gate_scope = gate_horizon_scope_for_method(
|
||||||
|
ctx.binding.primary_method_key, ctx.tenant_ctx, ctx.initiative_id
|
||||||
|
)
|
||||||
|
roadmap_items = roadmap_service.list_roadmap_items_for_initiative(
|
||||||
|
tenant_id=ctx.tenant_ctx.tenant_id, initiative_id=ctx.initiative_id
|
||||||
|
)
|
||||||
|
actions = ctx.actions
|
||||||
|
|
||||||
|
scoped_roadmap = roadmap_items
|
||||||
|
scoped_actions = actions
|
||||||
|
if gate_scope:
|
||||||
|
scope = str(gate_scope)
|
||||||
|
scoped_roadmap = [ri for ri in roadmap_items if str(ri.get("id")) == scope]
|
||||||
|
scoped_actions = [
|
||||||
|
a
|
||||||
|
for a in actions
|
||||||
|
if a.get("roadmap_item_id") and str(a["roadmap_item_id"]) == scope
|
||||||
|
]
|
||||||
|
|
||||||
|
return compute_planning_debt(actions=scoped_actions, roadmap_items=scoped_roadmap)
|
||||||
|
|
@ -16,6 +16,7 @@ class ReadModelProvider:
|
||||||
requires_elements: frozenset[str]
|
requires_elements: frozenset[str]
|
||||||
requires_data_slices: frozenset[str]
|
requires_data_slices: frozenset[str]
|
||||||
compute: ReadModelCompute
|
compute: ReadModelCompute
|
||||||
|
requires_any_element: frozenset[str] = frozenset()
|
||||||
|
|
||||||
|
|
||||||
_PROVIDERS: list[ReadModelProvider] = []
|
_PROVIDERS: list[ReadModelProvider] = []
|
||||||
|
|
@ -34,6 +35,10 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
|
||||||
for provider in _PROVIDERS:
|
for provider in _PROVIDERS:
|
||||||
if provider.requires_elements and not provider.requires_elements <= elements:
|
if provider.requires_elements and not provider.requires_elements <= elements:
|
||||||
continue
|
continue
|
||||||
|
if provider.requires_any_element and not (
|
||||||
|
provider.requires_any_element & elements
|
||||||
|
):
|
||||||
|
continue
|
||||||
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
||||||
continue
|
continue
|
||||||
result[provider.key] = provider.compute(ctx)
|
result[provider.key] = provider.compute(ctx)
|
||||||
|
|
@ -43,6 +48,8 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
|
||||||
|
|
||||||
def _register_builtin_read_models() -> None:
|
def _register_builtin_read_models() -> None:
|
||||||
from steering.read_models.epic_rollup import compute_epic_rollup
|
from steering.read_models.epic_rollup import compute_epic_rollup
|
||||||
|
from steering.read_models.execution_graph import compute_execution_graph_read_model
|
||||||
|
from steering.read_models.planning_debt import compute_planning_debt_read_model
|
||||||
|
|
||||||
register_read_model(
|
register_read_model(
|
||||||
ReadModelProvider(
|
ReadModelProvider(
|
||||||
|
|
@ -55,6 +62,23 @@ def _register_builtin_read_models() -> None:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
register_read_model(
|
||||||
|
ReadModelProvider(
|
||||||
|
key="planning_debt",
|
||||||
|
requires_elements=frozenset({"gate_fulfillment"}),
|
||||||
|
requires_data_slices=frozenset({"roadmap", "actions"}),
|
||||||
|
compute=compute_planning_debt_read_model,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
register_read_model(
|
||||||
|
ReadModelProvider(
|
||||||
|
key="execution_graph",
|
||||||
|
requires_elements=frozenset(),
|
||||||
|
requires_any_element=frozenset({"critical_path", "gate_fulfillment"}),
|
||||||
|
requires_data_slices=frozenset({"actions"}),
|
||||||
|
compute=compute_execution_graph_read_model,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_register_builtin_read_models()
|
_register_builtin_read_models()
|
||||||
|
|
|
||||||
93
backend/tests/test_gate_next_actions_unit.py
Normal file
93
backend/tests/test_gate_next_actions_unit.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Unit tests — gate_next_actions proposal."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
from steering.kernel.models import HorizonMarker, LifecycleContext, SteeringBinding
|
||||||
|
from steering.proposals.gate_next_actions import propose_gate_next_actions
|
||||||
|
from tenant_context import TenantContext
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(*, backlog_items, read_models=None, gate_id="gate-1"):
|
||||||
|
binding = SteeringBinding(
|
||||||
|
primary_method_key="sequential_dependency",
|
||||||
|
composition_modifier=None,
|
||||||
|
next_work_strategy_key="sequential_dependency",
|
||||||
|
)
|
||||||
|
lifecycle = LifecycleContext(
|
||||||
|
current_state="execution",
|
||||||
|
current_state_label="Ausführung",
|
||||||
|
slot_map={},
|
||||||
|
active_slots=(),
|
||||||
|
operational_slots=(),
|
||||||
|
)
|
||||||
|
horizon = HorizonMarker(
|
||||||
|
kind="gate",
|
||||||
|
gate_roadmap_item_id=gate_id,
|
||||||
|
gate_title="Gate A",
|
||||||
|
)
|
||||||
|
eval_ctx = SteeringEvalContext(
|
||||||
|
tenant_ctx=TenantContext(
|
||||||
|
user_id="u1",
|
||||||
|
email="t@example.com",
|
||||||
|
display_name="Test",
|
||||||
|
portal_role="user",
|
||||||
|
tenant_id="t1",
|
||||||
|
tenant_slug="t",
|
||||||
|
tenant_name="T",
|
||||||
|
tenant_role="admin",
|
||||||
|
actor_id=None,
|
||||||
|
actor_type=None,
|
||||||
|
session_token="test",
|
||||||
|
capabilities=frozenset(),
|
||||||
|
),
|
||||||
|
initiative_id="init-1",
|
||||||
|
binding=binding,
|
||||||
|
horizon=horizon,
|
||||||
|
lifecycle=lifecycle,
|
||||||
|
operating_context={
|
||||||
|
"steering_elements": ["gate_fulfillment", "critical_path"],
|
||||||
|
"data_slices": ["backlog", "roadmap", "actions"],
|
||||||
|
"backlog_vocabulary": {"convertible_kinds": ["story", "bug", "issue"]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
eval_ctx._backlog_items = backlog_items
|
||||||
|
eval_ctx._actions = []
|
||||||
|
eval_ctx._work_cycles = []
|
||||||
|
return eval_ctx, read_models or {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_proposal_includes_planning_debt():
|
||||||
|
eval_ctx, read_models = _ctx(
|
||||||
|
backlog_items=[],
|
||||||
|
read_models={
|
||||||
|
"planning_debt": [
|
||||||
|
{
|
||||||
|
"roadmap_item_id": "gate-1",
|
||||||
|
"title": "Gate A",
|
||||||
|
"message": "Kein Plan",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = propose_gate_next_actions(eval_ctx, read_models=read_models)
|
||||||
|
assert result[0]["scope_type"] == "roadmap_item"
|
||||||
|
assert result[0]["reason_code"] == "planning_debt"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_proposal_backlog_at_gate():
|
||||||
|
eval_ctx, read_models = _ctx(
|
||||||
|
backlog_items=[
|
||||||
|
{
|
||||||
|
"id": "b1",
|
||||||
|
"title": "Story",
|
||||||
|
"item_kind": "story",
|
||||||
|
"status": "accepted",
|
||||||
|
"roadmap_item_id": "gate-1",
|
||||||
|
"priority": "normal",
|
||||||
|
"sort_order": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
result = propose_gate_next_actions(eval_ctx, read_models=read_models)
|
||||||
|
assert any(p["scope_type"] == "backlog_item" for p in result)
|
||||||
57
backend/tests/test_intake_triage_unit.py
Normal file
57
backend/tests/test_intake_triage_unit.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Unit tests — intake_triage proposal."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from steering.eval_context import SteeringEvalContext
|
||||||
|
from steering.kernel.models import HorizonMarker, LifecycleContext, SteeringBinding
|
||||||
|
from steering.proposals.intake_triage import propose_intake_triage
|
||||||
|
from tenant_context import TenantContext
|
||||||
|
|
||||||
|
|
||||||
|
def test_intake_triage_proposes_backlog_items():
|
||||||
|
binding = SteeringBinding(
|
||||||
|
primary_method_key="sequential_dependency",
|
||||||
|
composition_modifier=None,
|
||||||
|
next_work_strategy_key="sequential_dependency",
|
||||||
|
)
|
||||||
|
eval_ctx = SteeringEvalContext(
|
||||||
|
tenant_ctx=TenantContext(
|
||||||
|
user_id="u1",
|
||||||
|
email="t@example.com",
|
||||||
|
display_name="Test",
|
||||||
|
portal_role="user",
|
||||||
|
tenant_id="t1",
|
||||||
|
tenant_slug="t",
|
||||||
|
tenant_name="T",
|
||||||
|
tenant_role="admin",
|
||||||
|
actor_id=None,
|
||||||
|
actor_type=None,
|
||||||
|
session_token="test",
|
||||||
|
capabilities=frozenset(),
|
||||||
|
),
|
||||||
|
initiative_id="init-1",
|
||||||
|
binding=binding,
|
||||||
|
horizon=HorizonMarker(kind="none"),
|
||||||
|
lifecycle=LifecycleContext(
|
||||||
|
current_state="execution",
|
||||||
|
current_state_label="Ausführung",
|
||||||
|
slot_map={},
|
||||||
|
active_slots=(),
|
||||||
|
operational_slots=(),
|
||||||
|
),
|
||||||
|
operating_context={
|
||||||
|
"steering_elements": ["gate_fulfillment"],
|
||||||
|
"data_slices": ["backlog"],
|
||||||
|
"backlog_vocabulary": {"convertible_kinds": ["story", "bug", "issue"]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
eval_ctx._backlog_items = [
|
||||||
|
{"id": "b1", "title": "Item", "item_kind": "story", "status": "new", "priority": "normal", "sort_order": 0},
|
||||||
|
]
|
||||||
|
eval_ctx._actions = []
|
||||||
|
eval_ctx._work_cycles = []
|
||||||
|
|
||||||
|
result = propose_intake_triage(eval_ctx, read_models={})
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["proposal_key"] == "intake_triage"
|
||||||
|
assert result[0]["ranker_key"] == "heuristic_v0"
|
||||||
134
docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md
Normal file
134
docs/architecture/ADP_Steering_Kernel_Coding_Rules_v0.1.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# ADP — Steering Kernel Coding Rules v0.1
|
||||||
|
|
||||||
|
**Status:** PO-freigegeben — **verbindlich für alle Coding-Agenten**
|
||||||
|
**Stand:** 2026-07-27
|
||||||
|
**Bezug:** ADP Steering Kernel Spine v0.1, ADP Steering Kernel Extension Model v0.1, ADP AP2.4, `.cursor/rules/kairo-steering-kernel.mdc`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Geltungsbereich
|
||||||
|
|
||||||
|
Diese Regeln gelten für **jede** Änderung an:
|
||||||
|
|
||||||
|
- `backend/steering/**` (Kernel, Registries, Provider, Ranker, Contributors)
|
||||||
|
- `backend/data_layer/initiative_snapshot.py` (Steuerungs-Read-Models)
|
||||||
|
- Frontend-Steuerungs-UI (`SteeringProposalsPanel`, Operating Context, Snapshot-Konsum)
|
||||||
|
|
||||||
|
**Ziel:** Ein gemeinsamer Steuerungskern für alle Methoden — keine Agile-Sonderlogik in Pages, kein Methoden-Fork im Kernel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Herzmaschine (Stop-the-line)
|
||||||
|
|
||||||
|
| Regel | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| **SK-01** | Initiative-Steuerung (Next Work, Attention, Read Models, Proposals) **nur** über `steering.kernel.evaluate_steering()`. |
|
||||||
|
| **SK-02** | Snapshot/API spiegeln Kernel-Output — **keine** parallele Berechnung in `initiative_snapshot.py` (Ausnahme: Legacy-Signals bis Migration). |
|
||||||
|
| **SK-03** | Keine Steuerungsheuristik in Routers, Services außerhalb `backend/steering/`, oder React-Pages. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Extension Registry (Pflicht-Muster)
|
||||||
|
|
||||||
|
Neue Steuerungsfähigkeit = **Provider registrieren**, nicht inline codieren.
|
||||||
|
|
||||||
|
| Typ | Registry | Aktivierung |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| Read Model | `steering/read_models/registry.py` | `requires_elements`, optional `requires_any_element`, `requires_data_slices` |
|
||||||
|
| Proposal | `steering/proposals/registry.py` | wie oben + optional `excludes_elements` |
|
||||||
|
| Attention | `steering/attention/contributors/registry.py` | `requires_elements` / `requires_any_element` |
|
||||||
|
| Ranker (Proposal) | `register_sprint_commit_ranker()` o. Ä. | Operating Context `proposal_rankers.{key}` |
|
||||||
|
|
||||||
|
**SK-04:** Provider-Schlüssel sind stabil (`sprint_commit`, `gate_next_actions`, `intake_triage`) — UI bindet an Keys, nicht an Archetyp.
|
||||||
|
|
||||||
|
**SK-05:** Keine feste Produktpolitik in Rankern (z. B. „Bugs immer zuerst“). Heuristik = Fallback mit `ranker_key: heuristic_v0`; Agent = `agent_v1` nach Principle Gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Proposal-DTO (Minimalvertrag)
|
||||||
|
|
||||||
|
Jeder Proposal-Eintrag liefert mindestens:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"proposal_key": str,
|
||||||
|
"scope_type": "backlog_item" | "roadmap_item" | "action" | ...,
|
||||||
|
"scope_id": str,
|
||||||
|
"rank": int,
|
||||||
|
"reason_code": str,
|
||||||
|
"summary": str,
|
||||||
|
"ranker_key": str,
|
||||||
|
"confidence": "heuristic" | "agent",
|
||||||
|
"factors": [{"code", "weight", "label"}],
|
||||||
|
"dependency_refs": [...],
|
||||||
|
"dependency_blocked": bool,
|
||||||
|
"data_source": "steering_kernel",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SK-06:** Proposals **mutieren nie** OM — Accept erfolgt explizit in UI/API.
|
||||||
|
|
||||||
|
**SK-07:** Abhängigkeiten (`parent_action_id`, Execution Graph, Epic) in `dependency_refs` / `dependency_blocked` — nicht stillschweigend ignorieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Methoden-Parität
|
||||||
|
|
||||||
|
| Methode / Element | Read Models | Proposals |
|
||||||
|
|-------------------|-------------|-----------|
|
||||||
|
| `work_cycle_scope` | epic_rollup (mit backlog_epic_hierarchy) | `sprint_commit` |
|
||||||
|
| `gate_fulfillment` | `planning_debt`, `execution_graph` | `gate_next_actions` |
|
||||||
|
| `backlog` ohne Sprint | — | `intake_triage` (`excludes work_cycle_scope`) |
|
||||||
|
| `queue_inbox` | — | `intake_triage` (Queue-Profil) |
|
||||||
|
| `critical_path` | `execution_graph` | (später path-basiert) |
|
||||||
|
|
||||||
|
**SK-08:** Neue Methode = Elemente im `MethodDefinition` + Provider — **kein** `if method_key == …` in Kernel.
|
||||||
|
|
||||||
|
**SK-09:** `agile_iteration` ist **Modifier**, keine Parallel-Welt — komponiert mit Primary (Product, Linear, …).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Frontend
|
||||||
|
|
||||||
|
**SK-10:** Steuerungs-UI liest `steeringSnapshot.steering_kernel.proposals` / `read_models` — nicht selbst sortieren.
|
||||||
|
|
||||||
|
**SK-11:** `SteeringProposalsPanel` + `PROPOSAL_UI_CONFIG` erweitern — **keine** neue Page pro Proposal-Typ.
|
||||||
|
|
||||||
|
**SK-12:** Capabilities aus `operatingContext.steering_elements` / `hasSteeringElement` — keine Archetyp-Ifs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. KI / Agent (eingefroren bis Principle Gate)
|
||||||
|
|
||||||
|
**SK-13:** Keine Prompts hardcodieren. Agent-Ranker implementieren als `ranker_key: agent_v1` + Actor-Slot — Kontext aus Snapshot/Operating Context.
|
||||||
|
|
||||||
|
**SK-14:** Bis Freigabe: Fallback auf `heuristic_v0` mit transparentem `factors[]`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Tests & Abnahme
|
||||||
|
|
||||||
|
**SK-15:** Jeder neue Provider: Unit-Test (reine Logik) + Integration über Snapshot wenn DB verfügbar.
|
||||||
|
|
||||||
|
**SK-16:** Truth Table + ADP Extension Model §6 aktualisieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Verboten (Anti-Patterns)
|
||||||
|
|
||||||
|
- Bug-zuerst / Story-zuerst als globale Sortierregel ohne `ranker_key`-Kennzeichnung
|
||||||
|
- Proposal-Logik in `BacklogSection` / `PlanSprintPage`
|
||||||
|
- Duplicate Read Model (Snapshot + Kernel)
|
||||||
|
- Neue OM-Tabelle für Vorschläge
|
||||||
|
- Methoden-spezifische Kernel-Forks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Referenzen
|
||||||
|
|
||||||
|
| Dokument | Pfad |
|
||||||
|
|----------|------|
|
||||||
|
| Kernel Extension Model | `docs/architecture/ADP_Steering_Kernel_Extension_Model_v0.1.md` |
|
||||||
|
| Kernel Spine | `docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md` |
|
||||||
|
| Cursor Rule | `.cursor/rules/kairo-steering-kernel.mdc` |
|
||||||
|
| Plugin-Architektur | `.cursor/rules/kairo-plugin-architecture.mdc` |
|
||||||
|
|
@ -210,7 +210,9 @@ class AgentSlotProvider:
|
||||||
| Extension | agile_iteration | continuous_product | sequential_dependency | program_delivery | care_navigation |
|
| Extension | agile_iteration | continuous_product | sequential_dependency | program_delivery | care_navigation |
|
||||||
|-----------|-----------------|--------------------|-----------------------|------------------|-----------------|
|
|-----------|-----------------|--------------------|-----------------------|------------------|-----------------|
|
||||||
| `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — |
|
| `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — |
|
||||||
| `sprint_commit` proposal | ✓ | — | — | — | — |
|
| `sprint_commit` proposal | ✓ (work_cycle_scope) | ✓ | — | ◐ | ◐ |
|
||||||
|
| `gate_next_actions` proposal | — | ◐ | ✓ | ✓ | — |
|
||||||
|
| `intake_triage` proposal | — | ✓ (excl. sprint) | ✓ | ◐ | ✓ |
|
||||||
| `planning_debt` | ◐ | ◐ | ✓ | ✓ | — |
|
| `planning_debt` | ◐ | ◐ | ✓ | ✓ | — |
|
||||||
| `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ |
|
| `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ |
|
||||||
| `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ |
|
| `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ |
|
||||||
|
|
@ -226,7 +228,7 @@ class AgentSlotProvider:
|
||||||
| Phase | Inhalt | Entkoppelt |
|
| Phase | Inhalt | Entkoppelt |
|
||||||
|-------|--------|------------|
|
|-------|--------|------------|
|
||||||
| **K-Ext-1** | `read_models/registry.py` + Kernel ruft Provider; P4 `epic_rollup` migriert | ✓ Kernel v0.3 |
|
| **K-Ext-1** | `read_models/registry.py` + Kernel ruft Provider; P4 `epic_rollup` migriert | ✓ Kernel v0.3 |
|
||||||
| **K-Ext-2** | `attention/contributors/registry.py`; inline-Attention refactoren | offen |
|
| **K-Ext-2** | `attention/contributors/registry.py`; inline-Attention refactoren | ✓ AP2.2j |
|
||||||
| **K-Ext-3** | `proposals/registry.py` + `SteeringEvaluation.proposals` | ✓ AP2.2i (P6) |
|
| **K-Ext-3** | `proposals/registry.py` + `SteeringEvaluation.proposals` | ✓ AP2.2i (P6) |
|
||||||
| **K-Ext-4** | `agent_slots/registry.py` + Operating Context Feld `agent_slots` | P8 Reviews; Principle Gate für KI |
|
| **K-Ext-4** | `agent_slots/registry.py` + Operating Context Feld `agent_slots` | P8 Reviews; Principle Gate für KI |
|
||||||
| **K-Ext-5** | Portfolio-Ebene: Workspace aggregiert Attention/Proposals über Initiativen | Program Director multi-initiative |
|
| **K-Ext-5** | Portfolio-Ebene: Workspace aggregiert Attention/Proposals über Initiativen | Program Director multi-initiative |
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,8 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1
|
||||||
|----|-----------|
|
|----|-----------|
|
||||||
| AP2.3/4 | ✓ Plugin-Architektur + Element-Registry (2026-07-25) |
|
| AP2.3/4 | ✓ Plugin-Architektur + Element-Registry (2026-07-25) |
|
||||||
| AP2.2a | Starter-Kits ◐→✓ |
|
| AP2.2a | Starter-Kits ◐→✓ |
|
||||||
| ADP Backlog/Epic | P1–P4 ✓ · P6 Sprint-Vorschlag ✓ · P5 Agent-Tasks offen · K-Ext-2…4 offen |
|
| ADP Backlog/Epic | P1–P4 ✓ · P6 Sprint-Vorschlag ✓ · K-Ext-1/2/3 ✓ · Agent-Slots offen |
|
||||||
|
| Steering Kernel Proposals | ◐ | sprint_commit · gate_next_actions · intake_triage (Kernel v0.3) |
|
||||||
| AP2.2b–e | Referenz-Archetypen End-to-End ◐→✓ |
|
| AP2.2b–e | Referenz-Archetypen End-to-End ◐→✓ |
|
||||||
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
||||||
| AP1.7b | Op-API Parität |
|
| AP1.7b | Op-API Parität |
|
||||||
|
|
|
||||||
107
frontend/src/components/SteeringProposalsPanel.jsx
Normal file
107
frontend/src/components/SteeringProposalsPanel.jsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import {
|
||||||
|
PROPOSAL_UI_CONFIG,
|
||||||
|
filterSprintProposals,
|
||||||
|
listProposalKeys,
|
||||||
|
proposalReasonLabel,
|
||||||
|
} from '../utils/steeringProposals.js'
|
||||||
|
import { backlogKindLabel, resolveBacklogVocabulary } from '../utils/resolveBacklogVocabulary.js'
|
||||||
|
import { PriorityBadge } from './PriorityBadge.jsx'
|
||||||
|
|
||||||
|
export function SteeringProposalsPanel({
|
||||||
|
proposalsByKey = {},
|
||||||
|
proposalKeys = null,
|
||||||
|
backlogVocabulary = null,
|
||||||
|
canManage = false,
|
||||||
|
onAcceptBacklogProposal,
|
||||||
|
busy = false,
|
||||||
|
filterContext = {},
|
||||||
|
}) {
|
||||||
|
const vocabulary = resolveBacklogVocabulary(backlogVocabulary)
|
||||||
|
const keys = proposalKeys || listProposalKeys(proposalsByKey)
|
||||||
|
|
||||||
|
if (!keys.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keys.map((proposalKey) => {
|
||||||
|
const config = PROPOSAL_UI_CONFIG[proposalKey] || {
|
||||||
|
title: proposalKey,
|
||||||
|
lead: 'Steuerungsvorschlag',
|
||||||
|
acceptLabel: 'Übernehmen',
|
||||||
|
scopeTypes: ['backlog_item'],
|
||||||
|
}
|
||||||
|
let items = proposalsByKey[proposalKey] || []
|
||||||
|
if (proposalKey === 'sprint_commit' && filterContext.workCycleId) {
|
||||||
|
items = filterSprintProposals(items, filterContext.workCycleId)
|
||||||
|
}
|
||||||
|
if (!items.length) return null
|
||||||
|
const ranker = items[0]?.ranker_key || 'heuristic_v0'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section key={proposalKey} className="card steering-proposals">
|
||||||
|
<div className="section-header">
|
||||||
|
<div>
|
||||||
|
<h2>{config.title}</h2>
|
||||||
|
<p className="section-lead muted">
|
||||||
|
{config.lead} Ranker: {ranker}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol className="item-list steering-proposals__list">
|
||||||
|
{items.map((proposal) => (
|
||||||
|
<li
|
||||||
|
key={`${proposalKey}-${proposal.scope_type}-${proposal.scope_id}`}
|
||||||
|
className="list-item card-list-item steering-proposal"
|
||||||
|
>
|
||||||
|
<div className="list-item-main">
|
||||||
|
<strong>
|
||||||
|
{proposal.rank}. {proposal.title}
|
||||||
|
</strong>
|
||||||
|
<p className="list-item-sub muted">{proposalReasonLabel(proposal)}</p>
|
||||||
|
{proposal.dependency_blocked && (
|
||||||
|
<p className="list-item-sub muted steering-proposal__blocked">
|
||||||
|
Abhängigkeit offen
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{proposal.scope_type === 'roadmap_item' && (
|
||||||
|
<p className="list-item-sub muted">
|
||||||
|
Gate ohne Plan — Arbeitspaket anlegen oder Eingang committen
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="list-item-meta action-controls">
|
||||||
|
{proposal.item_kind && (
|
||||||
|
<span className="badge badge--kind muted">
|
||||||
|
{backlogKindLabel(vocabulary, proposal.item_kind)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{proposal.priority && <PriorityBadge priority={proposal.priority} />}
|
||||||
|
{canManage
|
||||||
|
&& proposal.scope_type === 'backlog_item'
|
||||||
|
&& config.scopeTypes.includes('backlog_item') && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
disabled={busy || proposal.dependency_blocked}
|
||||||
|
onClick={() =>
|
||||||
|
onAcceptBacklogProposal?.(proposal.scope_id, {
|
||||||
|
work_cycle_id: proposal.work_cycle_id,
|
||||||
|
proposal_key: proposalKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{config.acceptLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
import { BacklogSection } from '../../components/BacklogSection.jsx'
|
import { BacklogSection } from '../../components/BacklogSection.jsx'
|
||||||
|
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
|
||||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
|
|
||||||
export function InitiativeInboxPage() {
|
export function InitiativeInboxPage() {
|
||||||
|
|
@ -21,8 +23,20 @@ export function InitiativeInboxPage() {
|
||||||
handleConvertBacklog,
|
handleConvertBacklog,
|
||||||
handleBulkConvertBacklog,
|
handleBulkConvertBacklog,
|
||||||
handleDeleteBacklog,
|
handleDeleteBacklog,
|
||||||
|
reloadSlices,
|
||||||
|
steeringSnapshot,
|
||||||
|
operatingContext,
|
||||||
} = useInitiativeOperations()
|
} = useInitiativeOperations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadSlices(['steering_snapshot'])
|
||||||
|
}, [reloadSlices])
|
||||||
|
|
||||||
|
const kernelProposals = useMemo(
|
||||||
|
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||||
|
[steeringSnapshot],
|
||||||
|
)
|
||||||
|
|
||||||
if (!capabilities.has('kairo.initiative.read')) {
|
if (!capabilities.has('kairo.initiative.read')) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +48,17 @@ export function InitiativeInboxPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
<SteeringProposalsPanel
|
||||||
|
proposalsByKey={kernelProposals}
|
||||||
|
proposalKeys={['intake_triage']}
|
||||||
|
backlogVocabulary={operatingContext?.backlog_vocabulary}
|
||||||
|
canManage={capabilities.has('kairo.backlog.manage')}
|
||||||
|
onAcceptBacklogProposal={async (itemId) => {
|
||||||
|
await handleConvertBacklog(itemId, {})
|
||||||
|
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||||
|
}}
|
||||||
|
busy={formBusy}
|
||||||
|
/>
|
||||||
<BacklogSection
|
<BacklogSection
|
||||||
items={backlogItems}
|
items={backlogItems}
|
||||||
initiativeId={initiativeId}
|
initiativeId={initiativeId}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
import { GatesPlanPanel } from '../../components/GatesPlanPanel.jsx'
|
import { GatesPlanPanel } from '../../components/GatesPlanPanel.jsx'
|
||||||
|
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
|
||||||
|
|
||||||
export function InitiativePlanPage() {
|
export function InitiativePlanPage() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -10,9 +12,22 @@ export function InitiativePlanPage() {
|
||||||
handleCreateRoadmapItem,
|
handleCreateRoadmapItem,
|
||||||
handleReorderRoadmapItems,
|
handleReorderRoadmapItems,
|
||||||
handleDeleteRoadmapItem,
|
handleDeleteRoadmapItem,
|
||||||
|
handleConvertBacklog,
|
||||||
|
reloadSlices,
|
||||||
|
steeringSnapshot,
|
||||||
|
operatingContext,
|
||||||
error,
|
error,
|
||||||
} = useInitiativeOperations()
|
} = useInitiativeOperations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadSlices(['steering_snapshot'])
|
||||||
|
}, [reloadSlices])
|
||||||
|
|
||||||
|
const kernelProposals = useMemo(
|
||||||
|
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||||
|
[steeringSnapshot],
|
||||||
|
)
|
||||||
|
|
||||||
if (!capabilities.has('kairo.initiative.read')) {
|
if (!capabilities.has('kairo.initiative.read')) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +35,17 @@ export function InitiativePlanPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
<SteeringProposalsPanel
|
||||||
|
proposalsByKey={kernelProposals}
|
||||||
|
proposalKeys={['gate_next_actions']}
|
||||||
|
backlogVocabulary={operatingContext?.backlog_vocabulary}
|
||||||
|
canManage={capabilities.has('kairo.backlog.manage')}
|
||||||
|
onAcceptBacklogProposal={async (itemId) => {
|
||||||
|
await handleConvertBacklog(itemId, {})
|
||||||
|
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||||
|
}}
|
||||||
|
busy={formBusy}
|
||||||
|
/>
|
||||||
<GatesPlanPanel
|
<GatesPlanPanel
|
||||||
initiativeId={initiativeId}
|
initiativeId={initiativeId}
|
||||||
items={roadmapItems}
|
items={roadmapItems}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'
|
||||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||||
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
||||||
import { SprintCommitProposalPanel } from '../../components/SprintCommitProposalPanel.jsx'
|
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
|
||||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||||
|
|
@ -58,11 +58,8 @@ function PlanSprintInner() {
|
||||||
reloadSlices(['steering_snapshot'])
|
reloadSlices(['steering_snapshot'])
|
||||||
}, [reloadSlices])
|
}, [reloadSlices])
|
||||||
|
|
||||||
const sprintCommitProposals = useMemo(
|
const kernelProposals = useMemo(
|
||||||
() =>
|
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||||
steeringSnapshot?.sprint_commit_proposals ||
|
|
||||||
steeringSnapshot?.steering_kernel?.proposals?.sprint_commit ||
|
|
||||||
[],
|
|
||||||
[steeringSnapshot],
|
[steeringSnapshot],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -110,12 +107,13 @@ function PlanSprintInner() {
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
/>
|
/>
|
||||||
{selectedWorkCycleId && (
|
{selectedWorkCycleId && (
|
||||||
<SprintCommitProposalPanel
|
<SteeringProposalsPanel
|
||||||
proposals={sprintCommitProposals}
|
proposalsByKey={kernelProposals}
|
||||||
selectedWorkCycleId={selectedWorkCycleId}
|
proposalKeys={['sprint_commit']}
|
||||||
|
filterContext={{ workCycleId: selectedWorkCycleId }}
|
||||||
backlogVocabulary={operatingContext?.backlog_vocabulary}
|
backlogVocabulary={operatingContext?.backlog_vocabulary}
|
||||||
canManage={capabilities.has('kairo.backlog.manage')}
|
canManage={capabilities.has('kairo.backlog.manage')}
|
||||||
onAcceptProposal={async (itemId, options) => {
|
onAcceptBacklogProposal={async (itemId, options) => {
|
||||||
await handleConvertBacklog(itemId, options)
|
await handleConvertBacklog(itemId, options)
|
||||||
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1716,10 +1716,28 @@
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sprint-commit-proposal__blocked {
|
.sprint-commit-proposal__blocked,
|
||||||
|
.steering-proposal__blocked {
|
||||||
color: var(--jk-warning-text, #7a4b08);
|
color: var(--jk-warning-text, #7a4b08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.steering-proposals {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-proposals__list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steering-proposal {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.archetype-badge {
|
.archetype-badge {
|
||||||
background: var(--jk-surface-muted, #eef2f6);
|
background: var(--jk-surface-muted, #eef2f6);
|
||||||
color: var(--jk-text-secondary, #445);
|
color: var(--jk-text-secondary, #445);
|
||||||
|
|
|
||||||
51
frontend/src/utils/steeringProposals.js
Normal file
51
frontend/src/utils/steeringProposals.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/** Generic steering proposal UI config (Kernel v0.3). */
|
||||||
|
|
||||||
|
export const PROPOSAL_UI_CONFIG = {
|
||||||
|
sprint_commit: {
|
||||||
|
title: 'Sprint-Vorschläge',
|
||||||
|
lead: 'Ranker-gestützte Commit-Vorschläge — Accept committet ins Sprint-Backlog.',
|
||||||
|
acceptLabel: 'In Sprint planen',
|
||||||
|
scopeTypes: ['backlog_item'],
|
||||||
|
},
|
||||||
|
gate_next_actions: {
|
||||||
|
title: 'Gate-Vorschläge',
|
||||||
|
lead: 'Planning Debt und Eingang am aktiven Gate — committen oder AP planen.',
|
||||||
|
acceptLabel: 'Committen',
|
||||||
|
scopeTypes: ['backlog_item'],
|
||||||
|
},
|
||||||
|
intake_triage: {
|
||||||
|
title: 'Triage-Vorschläge',
|
||||||
|
lead: 'Priorisierte Eingangs-Reihenfolge — Heuristik oder später Agent-Ranker.',
|
||||||
|
acceptLabel: 'Committen',
|
||||||
|
scopeTypes: ['backlog_item'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listProposalKeys(proposalsByKey = {}) {
|
||||||
|
return Object.keys(PROPOSAL_UI_CONFIG).filter(
|
||||||
|
(key) => Array.isArray(proposalsByKey[key]) && proposalsByKey[key].length > 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proposalReasonLabel(proposal) {
|
||||||
|
const labels = {
|
||||||
|
priority_critical: 'Dringlichkeit critical',
|
||||||
|
priority_high: 'Hohe Priorität',
|
||||||
|
candidate_bug_or_issue: 'Kandidat (Heuristik)',
|
||||||
|
candidate_ready: 'Commit-bereit (Heuristik)',
|
||||||
|
waiting_on_feature_action: 'Wartet auf Feature-AP',
|
||||||
|
missing_parent_action: 'Feature-Referenz fehlt',
|
||||||
|
dependency_blocked: 'Abhängigkeit offen',
|
||||||
|
gate_backlog_ready: 'Gate-Eingang committen',
|
||||||
|
planning_debt: 'Planning Debt — Plan ergänzen',
|
||||||
|
intake_triage_candidate: 'Triage-Kandidat',
|
||||||
|
}
|
||||||
|
return labels[proposal?.reason_code] || proposal?.summary || 'Vorschlag'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterSprintProposals(proposals, workCycleId) {
|
||||||
|
if (!workCycleId) return proposals || []
|
||||||
|
return (proposals || []).filter(
|
||||||
|
(p) => !p.work_cycle_id || String(p.work_cycle_id) === String(workCycleId),
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user