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)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
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`
|
||||
9. `docs/architecture/Kairo_Sprint0_Principle_Gate_v0.1.md`
|
||||
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**)
|
||||
12. `docs/architecture/ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md` (AP2.4 — **geliefert**)
|
||||
13. `.cursor/rules/kairo-plugin-architecture.mdc`
|
||||
|
|
|
|||
|
|
@ -362,6 +362,8 @@ def get_initiative_steering_snapshot(
|
|||
"active_work_cycle": active_work_cycle,
|
||||
"epic_rollup": epic_rollup,
|
||||
"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": {
|
||||
"actions_open": open_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 steering.kernel.models import HorizonMarker, SteeringBinding
|
||||
from steering.strategies.next_action.execution_ready import gate_horizon_scope_for_method
|
||||
from tenant_context import TenantContext
|
||||
|
||||
_GATE_METHODS = frozenset(
|
||||
{
|
||||
"sequential_dependency",
|
||||
"program_delivery",
|
||||
"maturity_progression",
|
||||
}
|
||||
)
|
||||
# Side-effect: register contributors
|
||||
import steering.attention.contributors.core # noqa: F401
|
||||
import steering.attention.contributors.gates # noqa: F401
|
||||
import steering.attention.contributors.read_models # noqa: F401
|
||||
|
||||
|
||||
def evaluate_attention(
|
||||
|
|
@ -29,161 +25,18 @@ def evaluate_attention(
|
|||
eval_ctx=None,
|
||||
limit: int = 20,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Kernel attention for a single initiative — no fake next when planning debt."""
|
||||
from services import actions as action_service
|
||||
from services import blockers as blocker_service
|
||||
from services import roadmap as roadmap_service
|
||||
from services.execution_plan import list_dependencies_for_initiative
|
||||
from steering.graph.execution_engine import (
|
||||
compute_execution_graph_state,
|
||||
compute_planning_debt,
|
||||
execution_waiting_to_attention_items,
|
||||
planning_debt_to_attention_items,
|
||||
"""Kernel attention — contributors via registry (K-Ext-2)."""
|
||||
from steering.attention.context import AttentionEvalContext
|
||||
from steering.attention.contributors.registry import collect_attention_items
|
||||
|
||||
actx = AttentionEvalContext(
|
||||
tenant_ctx=ctx,
|
||||
initiative_id=initiative_id,
|
||||
binding=binding,
|
||||
horizon=horizon,
|
||||
next_work=next_work,
|
||||
read_models=read_models or {},
|
||||
proposals=proposals or {},
|
||||
eval_ctx=eval_ctx,
|
||||
)
|
||||
|
||||
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]
|
||||
return collect_attention_items(actx)[: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 dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable
|
||||
|
||||
from steering.eval_context import SteeringEvalContext
|
||||
|
|
@ -16,6 +16,7 @@ class ProposalProvider:
|
|||
requires_elements: frozenset[str]
|
||||
requires_data_slices: frozenset[str]
|
||||
compute: ProposalCompute
|
||||
excludes_elements: frozenset[str] = field(default_factory=frozenset)
|
||||
|
||||
|
||||
_PROVIDERS: list[ProposalProvider] = []
|
||||
|
|
@ -37,6 +38,8 @@ def compute_proposals(
|
|||
for provider in _PROVIDERS:
|
||||
if provider.requires_elements and not provider.requires_elements <= elements:
|
||||
continue
|
||||
if provider.excludes_elements and provider.excludes_elements & elements:
|
||||
continue
|
||||
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
||||
continue
|
||||
result[provider.key] = provider.compute(ctx, read_models)
|
||||
|
|
@ -45,6 +48,8 @@ def compute_proposals(
|
|||
|
||||
|
||||
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
|
||||
|
||||
register_proposal(
|
||||
|
|
@ -52,7 +57,30 @@ def _register_builtin_proposals() -> None:
|
|||
key="sprint_commit",
|
||||
requires_elements=frozenset({"work_cycle_scope"}),
|
||||
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_data_slices: frozenset[str]
|
||||
compute: ReadModelCompute
|
||||
requires_any_element: frozenset[str] = frozenset()
|
||||
|
||||
|
||||
_PROVIDERS: list[ReadModelProvider] = []
|
||||
|
|
@ -34,6 +35,10 @@ def compute_read_models(ctx: SteeringEvalContext) -> dict[str, Any]:
|
|||
for provider in _PROVIDERS:
|
||||
if provider.requires_elements and not provider.requires_elements <= elements:
|
||||
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:
|
||||
continue
|
||||
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:
|
||||
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(
|
||||
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()
|
||||
|
|
|
|||
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 |
|
||||
|-----------|-----------------|--------------------|-----------------------|------------------|-----------------|
|
||||
| `epic_rollup` | ✓ | — | — | ◐ (Epic=Liefercontainer) | — |
|
||||
| `sprint_commit` proposal | ✓ | — | — | — | — |
|
||||
| `sprint_commit` proposal | ✓ (work_cycle_scope) | ✓ | — | ◐ | ◐ |
|
||||
| `gate_next_actions` proposal | — | ◐ | ✓ | ✓ | — |
|
||||
| `intake_triage` proposal | — | ✓ (excl. sprint) | ✓ | ◐ | ✓ |
|
||||
| `planning_debt` | ◐ | ◐ | ✓ | ✓ | — |
|
||||
| `execution_graph` | ◐ | ◐ | ✓ | ✓ | ◐ |
|
||||
| `intake_triage` proposal | ✓ | ✓ | ◐ | ◐ | ✓ |
|
||||
|
|
@ -226,7 +228,7 @@ class AgentSlotProvider:
|
|||
| Phase | Inhalt | Entkoppelt |
|
||||
|-------|--------|------------|
|
||||
| **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-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 |
|
||||
|
|
|
|||
|
|
@ -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.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.1 | MVP-Abnahfe ✗→✓ |
|
||||
| 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 { BacklogSection } from '../../components/BacklogSection.jsx'
|
||||
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
|
||||
export function InitiativeInboxPage() {
|
||||
|
|
@ -21,8 +23,20 @@ export function InitiativeInboxPage() {
|
|||
handleConvertBacklog,
|
||||
handleBulkConvertBacklog,
|
||||
handleDeleteBacklog,
|
||||
reloadSlices,
|
||||
steeringSnapshot,
|
||||
operatingContext,
|
||||
} = useInitiativeOperations()
|
||||
|
||||
useEffect(() => {
|
||||
reloadSlices(['steering_snapshot'])
|
||||
}, [reloadSlices])
|
||||
|
||||
const kernelProposals = useMemo(
|
||||
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||
[steeringSnapshot],
|
||||
)
|
||||
|
||||
if (!capabilities.has('kairo.initiative.read')) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -34,6 +48,17 @@ export function InitiativeInboxPage() {
|
|||
return (
|
||||
<>
|
||||
{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
|
||||
items={backlogItems}
|
||||
initiativeId={initiativeId}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useEffect, useMemo } from 'react'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { GatesPlanPanel } from '../../components/GatesPlanPanel.jsx'
|
||||
import { SteeringProposalsPanel } from '../../components/SteeringProposalsPanel.jsx'
|
||||
|
||||
export function InitiativePlanPage() {
|
||||
const {
|
||||
|
|
@ -10,9 +12,22 @@ export function InitiativePlanPage() {
|
|||
handleCreateRoadmapItem,
|
||||
handleReorderRoadmapItems,
|
||||
handleDeleteRoadmapItem,
|
||||
handleConvertBacklog,
|
||||
reloadSlices,
|
||||
steeringSnapshot,
|
||||
operatingContext,
|
||||
error,
|
||||
} = useInitiativeOperations()
|
||||
|
||||
useEffect(() => {
|
||||
reloadSlices(['steering_snapshot'])
|
||||
}, [reloadSlices])
|
||||
|
||||
const kernelProposals = useMemo(
|
||||
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||
[steeringSnapshot],
|
||||
)
|
||||
|
||||
if (!capabilities.has('kairo.initiative.read')) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -20,6 +35,17 @@ export function InitiativePlanPage() {
|
|||
return (
|
||||
<>
|
||||
{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
|
||||
initiativeId={initiativeId}
|
||||
items={roadmapItems}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react'
|
|||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.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 { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
|
|
@ -58,11 +58,8 @@ function PlanSprintInner() {
|
|||
reloadSlices(['steering_snapshot'])
|
||||
}, [reloadSlices])
|
||||
|
||||
const sprintCommitProposals = useMemo(
|
||||
() =>
|
||||
steeringSnapshot?.sprint_commit_proposals ||
|
||||
steeringSnapshot?.steering_kernel?.proposals?.sprint_commit ||
|
||||
[],
|
||||
const kernelProposals = useMemo(
|
||||
() => steeringSnapshot?.steering_kernel?.proposals || {},
|
||||
[steeringSnapshot],
|
||||
)
|
||||
|
||||
|
|
@ -110,12 +107,13 @@ function PlanSprintInner() {
|
|||
busy={formBusy}
|
||||
/>
|
||||
{selectedWorkCycleId && (
|
||||
<SprintCommitProposalPanel
|
||||
proposals={sprintCommitProposals}
|
||||
selectedWorkCycleId={selectedWorkCycleId}
|
||||
<SteeringProposalsPanel
|
||||
proposalsByKey={kernelProposals}
|
||||
proposalKeys={['sprint_commit']}
|
||||
filterContext={{ workCycleId: selectedWorkCycleId }}
|
||||
backlogVocabulary={operatingContext?.backlog_vocabulary}
|
||||
canManage={capabilities.has('kairo.backlog.manage')}
|
||||
onAcceptProposal={async (itemId, options) => {
|
||||
onAcceptBacklogProposal={async (itemId, options) => {
|
||||
await handleConvertBacklog(itemId, options)
|
||||
await reloadSlices(['steering_snapshot', 'actions', 'backlog'])
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1716,10 +1716,28 @@
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sprint-commit-proposal__blocked {
|
||||
.sprint-commit-proposal__blocked,
|
||||
.steering-proposal__blocked {
|
||||
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 {
|
||||
background: var(--jk-surface-muted, #eef2f6);
|
||||
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