feat(steering): Kernel Spine v0.1.1 als zentraler Initiative-Einstieg
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Failing after 3m33s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Failing after 3m33s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
evaluate_steering() vereinheitlicht Horizon, Next Work und Attention; Snapshot und Signal Engine delegieren dorthin; slot_map deklarativ fuer sequential_dependency und generic_operating. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
753f178b0c
commit
75a6a63665
|
|
@ -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`
|
||||
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`
|
||||
|
||||
## Vier Schichten — Auflösung zur Laufzeit
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ Verbindliche ADPs: `docs/architecture/ADP_Archetype_Method_Plugin_Architecture_v
|
|||
| **Archetyp** | `entity_archetypes` + `ui_profile_json` | Nav, Routen, Plan-Outline, `om_capabilities` |
|
||||
| **Methode** | `steering/methods/registrations/` | `data_slices`, `steering_elements`, `ui_features`, `graph_profile`, Strategie |
|
||||
| **Element** | `steering/elements/registry.py` + FE `steeringElementRegistry.js` | Control-/Work-Bausteine |
|
||||
| **Runtime** | `GET /api/initiatives/:id/operating-context` | Schnittmenge + Methoden-Vertrag |
|
||||
| **Runtime** | `GET /api/initiatives/:id/operating-context` + **`steering/kernel/evaluate_steering()`** | Plugin-Auflösung + **Herzmaschine** |
|
||||
|
||||
```text
|
||||
data_slices = archetype.om_capabilities ∩ method.data_slices
|
||||
|
|
@ -48,6 +48,7 @@ Archetyp-Default-Methode ist **Empfehlung**, nicht Lock — kompatible Methoden
|
|||
- Hardcodierte Methoden-UI in `methodUiDefaults.js` für **neue** Archetypen
|
||||
- Parallele Steuerungslogik außerhalb `backend/steering/`
|
||||
- Methoden-Dropdown ohne Archetyp-Filter (`GET /steering/methods?archetype_key=…`)
|
||||
- **Initiative-Steuerung** (Next, Attention) außerhalb `steering/kernel/evaluate_steering()` — siehe `ADP_Steering_Kernel_Spine_v0.1.md`
|
||||
|
||||
## Fallback (legacy)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ from typing import Any, Optional
|
|||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
from db import get_connection
|
||||
from data_layer.attention import get_next_action_candidates_for_initiative
|
||||
from entity_archetypes.registry import archetype_label
|
||||
from method_profiles.registry import get_method_profile
|
||||
from steering.context import get_steering_context_dto
|
||||
from steering.kernel import evaluate_steering
|
||||
from steering.signals.snapshot_signals import derive_initiative_signals
|
||||
from services.initiatives import get_initiative
|
||||
from tenant_context import TenantContext
|
||||
|
|
@ -293,9 +293,9 @@ def get_initiative_steering_snapshot(
|
|||
key=lambda m: (m["target_date"] is None, m["target_date"] or ""),
|
||||
)[:5]
|
||||
|
||||
next_actions = get_next_action_candidates_for_initiative(
|
||||
ctx, initiative_id=initiative_id, limit=5
|
||||
)
|
||||
evaluation = evaluate_steering(ctx, initiative_id=initiative_id, next_work_limit=5)
|
||||
next_actions = evaluation.next_work
|
||||
kernel_attention = evaluation.attention
|
||||
|
||||
steering = get_steering_context_dto(ctx, initiative_id=initiative_id)
|
||||
metadata = steering.get("lifecycle_metadata") or {}
|
||||
|
|
@ -335,8 +335,22 @@ def get_initiative_steering_snapshot(
|
|||
signals=signals,
|
||||
method_key=steering["method_key"],
|
||||
),
|
||||
"attention_items": _attention_items(signals),
|
||||
"attention_items": _attention_items(signals)
|
||||
+ [
|
||||
{
|
||||
"code": item.get("reason_code") or item.get("kind", "attention"),
|
||||
"label": item.get("summary") or item.get("title") or "Attention",
|
||||
}
|
||||
for item in kernel_attention[:5]
|
||||
],
|
||||
"signals": signals,
|
||||
"steering_kernel": {
|
||||
"horizon": evaluation.horizon.to_dict(),
|
||||
"lifecycle": evaluation.lifecycle.to_dict(),
|
||||
"primary_method_key": evaluation.binding.primary_method_key,
|
||||
"composition_modifier": evaluation.binding.composition_modifier,
|
||||
"data_source": evaluation.data_source,
|
||||
},
|
||||
"upcoming_milestones": upcoming_milestones,
|
||||
"upcoming_roadmap_items": upcoming_milestones,
|
||||
"next_actions": next_actions,
|
||||
|
|
|
|||
14
backend/steering/kernel/__init__.py
Normal file
14
backend/steering/kernel/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Steering Kernel Spine v0.1 — universal runtime entry for initiative steering."""
|
||||
|
||||
from steering.kernel.evaluate import evaluate_steering
|
||||
from steering.kernel.events import apply_steering_event
|
||||
from steering.kernel.models import HorizonMarker, LifecycleContext, SteeringBinding, SteeringEvaluation
|
||||
|
||||
__all__ = [
|
||||
"HorizonMarker",
|
||||
"LifecycleContext",
|
||||
"SteeringBinding",
|
||||
"SteeringEvaluation",
|
||||
"apply_steering_event",
|
||||
"evaluate_steering",
|
||||
]
|
||||
162
backend/steering/kernel/attention.py
Normal file
162
backend/steering/kernel/attention.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""Attention evaluation (Q5) — method- and horizon-aware for one initiative."""
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def evaluate_attention(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
binding: SteeringBinding,
|
||||
horizon: HorizonMarker,
|
||||
next_work: list[dict[str, Any]],
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
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]
|
||||
52
backend/steering/kernel/binding.py
Normal file
52
backend/steering/kernel/binding.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Resolve primary method and composition modifier."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from services import steering_context as sc_service
|
||||
from services.work_cycle import get_active_work_cycle
|
||||
from steering.kernel.models import SteeringBinding
|
||||
from steering.methods.registry import get_method
|
||||
from steering.strategies.next_action.default_strategy import default_strategy
|
||||
from tenant_context import TenantContext
|
||||
|
||||
_AGILE_KEY = "agile_iteration"
|
||||
|
||||
|
||||
def resolve_primary_method_key(ctx: TenantContext, initiative_id: str) -> str:
|
||||
row = sc_service.get_steering_context(
|
||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||
)
|
||||
if row:
|
||||
return row["method_key"]
|
||||
return "generic_operating"
|
||||
|
||||
|
||||
def _primary_composes_with_agile(primary_method_key: str) -> bool:
|
||||
agile = get_method(_AGILE_KEY)
|
||||
if not agile:
|
||||
return False
|
||||
return primary_method_key in agile.composes_with
|
||||
|
||||
|
||||
def resolve_steering_binding(ctx: TenantContext, initiative_id: str) -> SteeringBinding:
|
||||
primary_key = resolve_primary_method_key(ctx, initiative_id)
|
||||
primary = get_method(primary_key)
|
||||
primary_strategy = (
|
||||
primary.next_action_strategy_key if primary else default_strategy.key
|
||||
)
|
||||
|
||||
composition: str | None = None
|
||||
strategy_key = primary_strategy
|
||||
|
||||
active_cycle = get_active_work_cycle(
|
||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||
)
|
||||
if active_cycle and _primary_composes_with_agile(primary_key):
|
||||
composition = _AGILE_KEY
|
||||
strategy_key = _AGILE_KEY
|
||||
|
||||
return SteeringBinding(
|
||||
primary_method_key=primary_key,
|
||||
composition_modifier=composition,
|
||||
next_work_strategy_key=strategy_key,
|
||||
)
|
||||
66
backend/steering/kernel/evaluate.py
Normal file
66
backend/steering/kernel/evaluate.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Steering Kernel Spine — single entry point v0.1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from steering.kernel.attention import evaluate_attention
|
||||
from steering.kernel.binding import resolve_steering_binding
|
||||
from steering.kernel.horizon import resolve_horizon
|
||||
from steering.kernel.lifecycle import resolve_lifecycle_context
|
||||
from steering.kernel.models import SteeringEvaluation
|
||||
from steering.kernel.next_work import evaluate_next_work
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def evaluate_steering(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
next_work_limit: int = 10,
|
||||
attention_limit: int = 20,
|
||||
) -> SteeringEvaluation:
|
||||
"""
|
||||
Herzmaschine — ein Wahrheitsmodell pro Vorhaben.
|
||||
|
||||
Alle Initiative-Steuerung (Snapshot, Cockpit, API) muss hier durch.
|
||||
"""
|
||||
if next_work_limit < 1:
|
||||
next_work_limit = 1
|
||||
if attention_limit < 1:
|
||||
attention_limit = 1
|
||||
|
||||
binding = resolve_steering_binding(ctx, initiative_id)
|
||||
lifecycle = resolve_lifecycle_context(
|
||||
ctx, initiative_id=initiative_id, binding=binding
|
||||
)
|
||||
horizon = resolve_horizon(
|
||||
ctx,
|
||||
initiative_id=initiative_id,
|
||||
primary_method_key=binding.primary_method_key,
|
||||
composition_modifier=binding.composition_modifier,
|
||||
)
|
||||
|
||||
next_work = evaluate_next_work(
|
||||
ctx,
|
||||
initiative_id=initiative_id,
|
||||
binding=binding,
|
||||
horizon=horizon,
|
||||
limit=next_work_limit,
|
||||
)
|
||||
|
||||
attention = evaluate_attention(
|
||||
ctx,
|
||||
initiative_id=initiative_id,
|
||||
binding=binding,
|
||||
horizon=horizon,
|
||||
next_work=next_work,
|
||||
limit=attention_limit,
|
||||
)
|
||||
|
||||
return SteeringEvaluation(
|
||||
initiative_id=initiative_id,
|
||||
binding=binding,
|
||||
horizon=horizon,
|
||||
lifecycle=lifecycle,
|
||||
next_work=next_work,
|
||||
attention=attention,
|
||||
)
|
||||
42
backend/steering/kernel/events.py
Normal file
42
backend/steering/kernel/events.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Event ingress stub — dual trigger (Plan | Event) v0.1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from tenant_context import TenantContext
|
||||
|
||||
EventKind = Literal[
|
||||
"actor_result",
|
||||
"external_event",
|
||||
"cadence_due",
|
||||
"situational",
|
||||
]
|
||||
|
||||
|
||||
def apply_steering_event(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
event_kind: EventKind,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Event-Schub in den Kernel (Stub v0.1).
|
||||
|
||||
Plan-dominante Methoden: Event wird protokolliert, Auswertung erfolgt
|
||||
weiterhin über evaluate_steering (Plan-Zug). Volle Event-Pipeline folgt
|
||||
für dispute_procedure, care_navigation, recurring_control.
|
||||
"""
|
||||
from steering.kernel.evaluate import evaluate_steering
|
||||
|
||||
evaluation = evaluate_steering(ctx, initiative_id=initiative_id)
|
||||
return {
|
||||
"accepted": True,
|
||||
"event_kind": event_kind,
|
||||
"initiative_id": initiative_id,
|
||||
"handled": False,
|
||||
"message": "Event ingress stub — re-evaluate via Plan-Zug",
|
||||
"payload_keys": sorted((payload or {}).keys()),
|
||||
"evaluation": evaluation.to_dict(),
|
||||
}
|
||||
67
backend/steering/kernel/horizon.py
Normal file
67
backend/steering/kernel/horizon.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Horizon resolution (Q1)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from steering.kernel.models import HorizonMarker
|
||||
from steering.strategies.next_action.execution_ready import (
|
||||
first_active_gate_id,
|
||||
gate_horizon_scope_for_method,
|
||||
)
|
||||
from services.work_cycle import get_active_work_cycle
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def resolve_horizon(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
primary_method_key: str,
|
||||
composition_modifier: str | None,
|
||||
) -> HorizonMarker:
|
||||
gate_id = gate_horizon_scope_for_method(
|
||||
primary_method_key, ctx, initiative_id
|
||||
)
|
||||
gate_title: str | None = None
|
||||
if gate_id:
|
||||
gate_title = _load_gate_title(ctx, gate_id)
|
||||
|
||||
active_cycle = get_active_work_cycle(
|
||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||
)
|
||||
if composition_modifier and active_cycle:
|
||||
return HorizonMarker(
|
||||
kind="work_cycle",
|
||||
gate_roadmap_item_id=gate_id,
|
||||
gate_title=gate_title,
|
||||
work_cycle_id=str(active_cycle["id"]),
|
||||
work_cycle_title=active_cycle.get("title"),
|
||||
)
|
||||
|
||||
if gate_id:
|
||||
return HorizonMarker(
|
||||
kind="gate",
|
||||
gate_roadmap_item_id=gate_id,
|
||||
gate_title=gate_title,
|
||||
)
|
||||
|
||||
return HorizonMarker(kind="none")
|
||||
|
||||
|
||||
def _load_gate_title(ctx: TenantContext, gate_id: str) -> str | None:
|
||||
from db import get_connection
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT title FROM roadmap_items
|
||||
WHERE id = %s AND tenant_id = %s
|
||||
""",
|
||||
(gate_id, ctx.tenant_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row["title"] if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
65
backend/steering/kernel/lifecycle.py
Normal file
65
backend/steering/kernel/lifecycle.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Lifecycle slot resolution — Kernel §4 (declarative, v0.1.1)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from steering.kernel.models import LifecycleContext, SteeringBinding
|
||||
from steering.lifecycle.slot_maps import (
|
||||
active_slot_keys,
|
||||
is_slot_operational,
|
||||
slot_map_from_tuple,
|
||||
)
|
||||
from steering.lifecycle.states import STANDARD_LIFECYCLE_STEPS, lifecycle_label
|
||||
from steering.methods.registry import get_method
|
||||
from services import steering_context as sc_service
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def _derive_slot_map_from_steps(steps: tuple[str, ...]) -> dict[str, str]:
|
||||
active = set(steps)
|
||||
return {
|
||||
step: ("active" if step in active else "n/a") for step in STANDARD_LIFECYCLE_STEPS
|
||||
}
|
||||
|
||||
|
||||
def resolve_method_slot_map(method_key: str) -> dict[str, str]:
|
||||
method = get_method(method_key)
|
||||
if not method:
|
||||
return _derive_slot_map_from_steps(STANDARD_LIFECYCLE_STEPS)
|
||||
if method.slot_map:
|
||||
return slot_map_from_tuple(method.slot_map)
|
||||
return _derive_slot_map_from_steps(method.default_lifecycle_steps)
|
||||
|
||||
|
||||
def resolve_lifecycle_context(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
binding: SteeringBinding,
|
||||
) -> LifecycleContext:
|
||||
slot_map = resolve_method_slot_map(binding.primary_method_key)
|
||||
|
||||
row = sc_service.get_steering_context(
|
||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||
)
|
||||
current_state = row["lifecycle_state"] if row else "intake"
|
||||
if current_state not in STANDARD_LIFECYCLE_STEPS:
|
||||
current_state = "intake"
|
||||
|
||||
active = active_slot_keys(slot_map) # type: ignore[arg-type]
|
||||
operational = tuple(
|
||||
step for step in STANDARD_LIFECYCLE_STEPS if is_slot_operational(slot_map.get(step, "n/a")) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
return LifecycleContext(
|
||||
current_state=current_state,
|
||||
current_state_label=lifecycle_label(current_state),
|
||||
slot_map=slot_map,
|
||||
active_slots=active,
|
||||
operational_slots=operational,
|
||||
)
|
||||
|
||||
|
||||
def lifecycle_to_dict(lifecycle: LifecycleContext) -> dict[str, Any]:
|
||||
return lifecycle.to_dict()
|
||||
81
backend/steering/kernel/models.py
Normal file
81
backend/steering/kernel/models.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""Steering Kernel Spine — data models v0.1.1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HorizonMarker:
|
||||
"""Aktiver Steuerungs-Horizont (Q1)."""
|
||||
|
||||
kind: Literal["gate", "work_cycle", "none"]
|
||||
gate_roadmap_item_id: str | None = None
|
||||
gate_title: str | None = None
|
||||
work_cycle_id: str | None = None
|
||||
work_cycle_title: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": self.kind,
|
||||
"gate_roadmap_item_id": self.gate_roadmap_item_id,
|
||||
"gate_title": self.gate_title,
|
||||
"work_cycle_id": self.work_cycle_id,
|
||||
"work_cycle_title": self.work_cycle_title,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SteeringBinding:
|
||||
"""Aufgelöste Methode: Primary + optional Composition."""
|
||||
|
||||
primary_method_key: str
|
||||
composition_modifier: str | None
|
||||
next_work_strategy_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LifecycleContext:
|
||||
"""Deklarativer Lifecycle-Vertrag + persistierter Pointer."""
|
||||
|
||||
current_state: str
|
||||
current_state_label: str
|
||||
slot_map: dict[str, str]
|
||||
active_slots: tuple[str, ...]
|
||||
operational_slots: tuple[str, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"current_state": self.current_state,
|
||||
"current_state_label": self.current_state_label,
|
||||
"slot_map": dict(self.slot_map),
|
||||
"active_slots": list(self.active_slots),
|
||||
"operational_slots": list(self.operational_slots),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SteeringEvaluation:
|
||||
"""Ergebnis von evaluate_steering — ein Wahrheitsmodell pro Vorhaben."""
|
||||
|
||||
initiative_id: str
|
||||
binding: SteeringBinding
|
||||
horizon: HorizonMarker
|
||||
lifecycle: LifecycleContext
|
||||
next_work: list[dict[str, Any]] = field(default_factory=list)
|
||||
attention: list[dict[str, Any]] = field(default_factory=list)
|
||||
data_source: str = "steering_kernel_v0.1.1"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"initiative_id": self.initiative_id,
|
||||
"primary_method_key": self.binding.primary_method_key,
|
||||
"composition_modifier": self.binding.composition_modifier,
|
||||
"next_work_strategy_key": self.binding.next_work_strategy_key,
|
||||
"horizon": self.horizon.to_dict(),
|
||||
"lifecycle": self.lifecycle.to_dict(),
|
||||
"next_work": self.next_work,
|
||||
"attention": self.attention,
|
||||
"data_source": self.data_source,
|
||||
}
|
||||
37
backend/steering/kernel/next_work.py
Normal file
37
backend/steering/kernel/next_work.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Next work evaluation (Schlitz 5) — routes to registered strategies only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from steering.kernel.models import HorizonMarker, SteeringBinding
|
||||
from steering.strategies.next_action.default_strategy import default_strategy
|
||||
from steering.strategies.next_action.registry import get_next_action_strategy
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def evaluate_next_work(
|
||||
ctx: TenantContext,
|
||||
*,
|
||||
initiative_id: str,
|
||||
binding: SteeringBinding,
|
||||
horizon: HorizonMarker,
|
||||
limit: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Invoke the bound next-work strategy (primary or composition)."""
|
||||
if limit < 1:
|
||||
limit = 1
|
||||
|
||||
strategy = get_next_action_strategy(binding.next_work_strategy_key) or default_strategy
|
||||
candidates = strategy.evaluate(
|
||||
ctx, initiative_id=initiative_id, limit=limit
|
||||
)
|
||||
|
||||
for item in candidates:
|
||||
item.setdefault("data_source", "steering_kernel")
|
||||
if horizon.gate_roadmap_item_id:
|
||||
item.setdefault("horizon_gate_id", horizon.gate_roadmap_item_id)
|
||||
if horizon.work_cycle_id:
|
||||
item.setdefault("horizon_work_cycle_id", horizon.work_cycle_id)
|
||||
|
||||
return candidates[:limit]
|
||||
76
backend/steering/lifecycle/slot_maps.py
Normal file
76
backend/steering/lifecycle/slot_maps.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Method slot maps — Kernel §4 / Spec-D D5."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from steering.lifecycle.states import STANDARD_LIFECYCLE_STEPS
|
||||
|
||||
SlotStatus = Literal["active", "skip", "n/a", "optional"]
|
||||
|
||||
VALID_SLOT_STATUSES = frozenset({"active", "skip", "n/a", "optional"})
|
||||
|
||||
# Spec-D sequential_dependency D5
|
||||
SEQUENTIAL_DEPENDENCY_SLOTS: dict[str, SlotStatus] = {
|
||||
"intake": "active",
|
||||
"method_selection": "active",
|
||||
"structure_setup": "active",
|
||||
"planning": "active",
|
||||
"action_selection": "active",
|
||||
"assignment": "active",
|
||||
"waiting": "active",
|
||||
"result_intake": "active",
|
||||
"validation": "active",
|
||||
"review": "optional",
|
||||
"adaptation": "active",
|
||||
"closure": "active",
|
||||
}
|
||||
|
||||
# Spec-D generic_operating D5 (method_bind → method_selection, next_work → action_selection)
|
||||
GENERIC_OPERATING_SLOTS: dict[str, SlotStatus] = {
|
||||
"intake": "active",
|
||||
"method_selection": "active",
|
||||
"structure_setup": "optional",
|
||||
"planning": "n/a",
|
||||
"action_selection": "active",
|
||||
"assignment": "active",
|
||||
"waiting": "optional",
|
||||
"result_intake": "active",
|
||||
"validation": "n/a",
|
||||
"review": "n/a",
|
||||
"adaptation": "optional",
|
||||
"closure": "optional",
|
||||
}
|
||||
|
||||
|
||||
def validate_slot_map(slot_map: dict[str, SlotStatus]) -> None:
|
||||
for step, status in slot_map.items():
|
||||
if step not in STANDARD_LIFECYCLE_STEPS:
|
||||
raise ValueError(f"Unknown lifecycle slot: {step}")
|
||||
if status not in VALID_SLOT_STATUSES:
|
||||
raise ValueError(f"Invalid slot status for {step}: {status}")
|
||||
missing = set(STANDARD_LIFECYCLE_STEPS) - set(slot_map.keys())
|
||||
if missing:
|
||||
raise ValueError(f"slot_map missing lifecycle steps: {sorted(missing)}")
|
||||
|
||||
|
||||
def slot_map_to_tuple(slot_map: dict[str, SlotStatus]) -> tuple[tuple[str, SlotStatus], ...]:
|
||||
validate_slot_map(slot_map)
|
||||
return tuple((step, slot_map[step]) for step in STANDARD_LIFECYCLE_STEPS)
|
||||
|
||||
|
||||
def slot_map_from_tuple(items: tuple[tuple[str, SlotStatus], ...]) -> dict[str, SlotStatus]:
|
||||
if not items:
|
||||
return {}
|
||||
slot_map = dict(items)
|
||||
validate_slot_map(slot_map)
|
||||
return slot_map
|
||||
|
||||
|
||||
def active_slot_keys(slot_map: dict[str, SlotStatus]) -> tuple[str, ...]:
|
||||
return tuple(step for step in STANDARD_LIFECYCLE_STEPS if slot_map.get(step) == "active")
|
||||
|
||||
|
||||
def is_slot_operational(status: SlotStatus) -> bool:
|
||||
"""Slots that participate in runtime evaluation (v0.1.1)."""
|
||||
return status in ("active", "optional")
|
||||
|
|
@ -97,6 +97,7 @@ def register_stub_method(
|
|||
graph_profile: GraphMethodProfile = LIGHT,
|
||||
method_role: Literal["primary", "modifier"] = "primary",
|
||||
composes_with: frozenset[str] | None = None,
|
||||
slot_map: tuple[tuple[str, str], ...] | None = None,
|
||||
) -> None:
|
||||
if get_method(key):
|
||||
return
|
||||
|
|
@ -115,6 +116,7 @@ def register_stub_method(
|
|||
graph_profile=graph_profile,
|
||||
method_role=method_role,
|
||||
composes_with=composes_with or frozenset(),
|
||||
slot_map=slot_map or (),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from steering.graph.profiles import FULFILLMENT, LIGHT, STRICT
|
||||
from steering.lifecycle.slot_maps import (
|
||||
SEQUENTIAL_DEPENDENCY_SLOTS,
|
||||
slot_map_to_tuple,
|
||||
)
|
||||
from steering.methods.registrations._helpers import (
|
||||
ELEM_NEXT,
|
||||
SLICES_AGILE,
|
||||
|
|
@ -37,6 +41,7 @@ def register() -> None:
|
|||
| frozenset({"critical_path", "gate_fulfillment"}),
|
||||
ui_features=frozenset({"criticalPathControl"}),
|
||||
graph_profile=STRICT,
|
||||
slot_map=slot_map_to_tuple(SEQUENTIAL_DEPENDENCY_SLOTS),
|
||||
)
|
||||
register_stub_method(
|
||||
key="recurring_control",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from steering.lifecycle.states import STANDARD_LIFECYCLE_STEPS
|
||||
from steering.graph.profiles import LIGHT
|
||||
from steering.lifecycle.slot_maps import GENERIC_OPERATING_SLOTS, slot_map_to_tuple
|
||||
from steering.lifecycle.states import STANDARD_LIFECYCLE_STEPS
|
||||
from steering.methods.registrations._helpers import ELEM_NEXT, SLICES_GENERIC
|
||||
from steering.methods.registry import MethodDefinition, get_method, register_method
|
||||
|
||||
|
|
@ -23,5 +24,6 @@ def register() -> None:
|
|||
compatible_archetype_keys="*",
|
||||
steering_elements=ELEM_NEXT,
|
||||
graph_profile=LIGHT,
|
||||
slot_map=slot_map_to_tuple(GENERIC_OPERATING_SLOTS),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class MethodDefinition:
|
|||
graph_profile: GraphMethodProfile = LIGHT
|
||||
method_role: Literal["primary", "modifier"] = "primary"
|
||||
composes_with: frozenset[str] = frozenset()
|
||||
slot_map: tuple[tuple[str, str], ...] = ()
|
||||
|
||||
|
||||
def ui_features_as_dict(features: frozenset[str]) -> dict[str, bool]:
|
||||
|
|
@ -70,6 +71,10 @@ def register_method(defn: MethodDefinition) -> None:
|
|||
raise ValueError(
|
||||
f"Modifier method {defn.key} must declare composes_with primary keys"
|
||||
)
|
||||
if defn.slot_map:
|
||||
from steering.lifecycle.slot_maps import slot_map_from_tuple
|
||||
|
||||
slot_map_from_tuple(defn.slot_map)
|
||||
_METHODS[defn.key] = defn
|
||||
|
||||
|
||||
|
|
@ -133,6 +138,10 @@ def method_to_dict(
|
|||
"composes_with": sorted(method.composes_with),
|
||||
"data_slices": sorted(method.data_slices),
|
||||
}
|
||||
if method.slot_map:
|
||||
payload["slot_map"] = {
|
||||
step: status for step, status in method.slot_map
|
||||
}
|
||||
if include_compatibility:
|
||||
compat = method.compatible_archetype_keys
|
||||
payload["compatible_archetype_keys"] = (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
"""Signal Engine — method-aware NextAction (AP1.1)."""
|
||||
"""Signal Engine — delegates initiative steering to Kernel Spine v0.1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from services import steering_context as sc_service
|
||||
from steering.methods.registry import get_method
|
||||
from steering.kernel import evaluate_steering
|
||||
from steering.signals import default_rules
|
||||
from steering.strategies.next_action.default_strategy import default_strategy
|
||||
from steering.strategies.next_action.registry import get_next_action_strategy
|
||||
|
|
@ -14,43 +13,6 @@ from tenant_context import TenantContext
|
|||
SignalKind = Literal["attention", "next_action"]
|
||||
|
||||
|
||||
def _resolve_method_key(ctx: TenantContext, initiative_id: str | None) -> str:
|
||||
if not initiative_id:
|
||||
return "generic_operating"
|
||||
row = sc_service.get_steering_context(
|
||||
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||
)
|
||||
if row:
|
||||
return row["method_key"]
|
||||
return "generic_operating"
|
||||
|
||||
|
||||
def _primary_composes_with_agile(method_key: str) -> bool:
|
||||
agile = get_method("agile_iteration")
|
||||
if not agile:
|
||||
return False
|
||||
return method_key in agile.composes_with
|
||||
|
||||
|
||||
def _resolve_next_action_strategy_key(
|
||||
ctx: TenantContext, initiative_id: str | None
|
||||
) -> str:
|
||||
method_key = _resolve_method_key(ctx, initiative_id)
|
||||
method = get_method(method_key)
|
||||
strategy_key = method.next_action_strategy_key if method else default_strategy.key
|
||||
if not initiative_id:
|
||||
return strategy_key
|
||||
|
||||
from services.work_cycle import get_active_work_cycle
|
||||
|
||||
active = get_active_work_cycle(tenant_id=ctx.tenant_id, initiative_id=initiative_id)
|
||||
if active and _primary_composes_with_agile(method_key):
|
||||
agile = get_next_action_strategy("agile_iteration")
|
||||
if agile:
|
||||
return agile.key
|
||||
return strategy_key
|
||||
|
||||
|
||||
def evaluate(
|
||||
ctx: TenantContext,
|
||||
kind: SignalKind = "attention",
|
||||
|
|
@ -58,9 +20,19 @@ def evaluate(
|
|||
limit: int = 10,
|
||||
initiative_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if initiative_id:
|
||||
evaluation = evaluate_steering(
|
||||
ctx,
|
||||
initiative_id=initiative_id,
|
||||
next_work_limit=limit,
|
||||
attention_limit=max(limit, 10),
|
||||
)
|
||||
if kind == "next_action":
|
||||
return evaluation.next_work
|
||||
return evaluation.attention
|
||||
|
||||
if kind == "attention":
|
||||
return default_rules.get_attention_items(ctx)
|
||||
|
||||
strategy_key = _resolve_next_action_strategy_key(ctx, initiative_id)
|
||||
strategy = get_next_action_strategy(strategy_key) or default_strategy
|
||||
return strategy.evaluate(ctx, initiative_id=initiative_id, limit=limit)
|
||||
strategy = get_next_action_strategy("default") or default_strategy
|
||||
return strategy.evaluate(ctx, initiative_id=None, limit=limit)
|
||||
|
|
|
|||
111
backend/tests/test_ap_kernel_spine.py
Normal file
111
backend/tests/test_ap_kernel_spine.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""Steering Kernel Spine v0.1 — unit and integration tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from steering.kernel import apply_steering_event, evaluate_steering
|
||||
from steering.kernel.binding import resolve_steering_binding
|
||||
from tests.factories import provision_user_in_tenant
|
||||
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||
|
||||
|
||||
def test_evaluate_steering_linear_binding(client):
|
||||
user = provision_user_in_tenant(tenant_role="member")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Kernel Binding",
|
||||
archetype_key="initiative.linear_project",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
from tenant_context import TenantContext
|
||||
|
||||
ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"])
|
||||
binding = resolve_steering_binding(ctx, initiative_id)
|
||||
assert binding.primary_method_key == "sequential_dependency"
|
||||
assert binding.composition_modifier is None
|
||||
assert binding.next_work_strategy_key == "sequential_dependency"
|
||||
|
||||
|
||||
def test_evaluate_steering_returns_horizon_and_next(client):
|
||||
user = provision_user_in_tenant(tenant_role="admin")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Kernel Eval",
|
||||
archetype_key="initiative.linear_project",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
from tenant_context import TenantContext
|
||||
|
||||
ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"])
|
||||
evaluation = evaluate_steering(ctx, initiative_id=initiative_id)
|
||||
assert evaluation.data_source == "steering_kernel_v0.1.1"
|
||||
assert evaluation.lifecycle.slot_map.get("action_selection") == "active"
|
||||
assert evaluation.lifecycle.slot_map.get("validation") == "active"
|
||||
assert evaluation.lifecycle.slot_map.get("review") == "optional"
|
||||
assert evaluation.horizon.kind in ("gate", "none", "work_cycle")
|
||||
if evaluation.horizon.kind == "gate":
|
||||
assert evaluation.horizon.gate_roadmap_item_id
|
||||
|
||||
snap = client.get(
|
||||
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert snap.status_code == 200
|
||||
body = snap.json()
|
||||
assert body.get("steering_kernel")
|
||||
assert body["steering_kernel"]["primary_method_key"] == "sequential_dependency"
|
||||
assert body["next_actions"] == evaluation.next_work
|
||||
|
||||
|
||||
def test_snapshot_routes_through_kernel_not_side_path(client):
|
||||
user = provision_user_in_tenant(tenant_role="admin")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Kernel Path",
|
||||
archetype_key="initiative.linear_project",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
snap = client.get(
|
||||
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert snap.json()["steering_kernel"]["data_source"] == "steering_kernel_v0.1.1"
|
||||
assert "lifecycle" in snap.json()["steering_kernel"]
|
||||
assert snap.json()["steering_kernel"]["lifecycle"]["slot_map"]["closure"] == "active"
|
||||
|
||||
|
||||
def test_apply_steering_event_stub(client):
|
||||
user = provision_user_in_tenant(tenant_role="member")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Event Stub",
|
||||
archetype_key="initiative.linear_project",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
from tenant_context import TenantContext
|
||||
|
||||
ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"])
|
||||
result = apply_steering_event(
|
||||
ctx,
|
||||
initiative_id=initiative_id,
|
||||
event_kind="actor_result",
|
||||
payload={"action_id": "test"},
|
||||
)
|
||||
assert result["accepted"] is True
|
||||
assert result["handled"] is False
|
||||
assert result["evaluation"]["data_source"] == "steering_kernel_v0.1.1"
|
||||
93
docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md
Normal file
93
docs/architecture/ADP_Steering_Kernel_Spine_v0.1.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# ADP — Steering Kernel Spine v0.1
|
||||
|
||||
**Status:** PO-freigegeben — **Implementierung aktiv** (2026-07-26)
|
||||
**Stand:** 2026-07-26
|
||||
**Bezug:** `Kairo_Steering_Method_Kernel_v0.1.md`, `ADP_AP2_4_Steering_Elements_and_Method_Contract_v0.1.md`, Normierungsprogramm M5
|
||||
**Ersetzt nicht:** AP2.3/AP2.4 Plugin-Schicht — **setzt darauf auf**
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem
|
||||
|
||||
Steuerungslogik (Next Action, Attention, Horizont) entsteht heute **verstreut** in Strategies, `signals/engine.py` und Snapshot-Hilfsregeln. Es gibt **keinen** zentralen Runtime-Einstieg — Produkt wirkt unführbar, Spec-Kern (M1c) ist nicht im Code.
|
||||
|
||||
**PO-Entscheidung:** Erst **Herzmaschine** (Kernel Spine), dann Archetyp-Slices und UI.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope Lock — Spine v0.1 (schmal)
|
||||
|
||||
### In Scope
|
||||
|
||||
| # | Lieferung |
|
||||
|---|-----------|
|
||||
| K1 | `backend/steering/kernel/` — **einziger** Einstieg `evaluate_steering()` pro Vorhaben |
|
||||
| K2 | `SteeringBinding` — Primary-Methode + optional Composition (`agile_iteration`) |
|
||||
| K3 | `HorizonMarker` — aktives Gate / aktiver Sprint |
|
||||
| K4 | `next_work` + `attention` — nur über Spine; Snapshot/API für Initiative |
|
||||
| K5 | Event-Ingress **Stub** `apply_steering_event()` (Plan-Methoden: no-op) |
|
||||
| K6 | Referenz-Abnahme: **A2** + `sequential_dependency` (+ Agile-Komposition) |
|
||||
|
||||
### Out of Scope (v0.1)
|
||||
|
||||
- Voller 12-Schritt-Lifecycle-Orchestrator als Schleife
|
||||
- Nested Programm-Impulse (B2a P0)
|
||||
- Event-Methoden C1/Care voll
|
||||
- Neue OM-Tabellen
|
||||
- Frontend-Umbau (nutzt bestehende Snapshot-Felder)
|
||||
|
||||
---
|
||||
|
||||
## 3. Architektur
|
||||
|
||||
```text
|
||||
GET …/steering-snapshot ─┐
|
||||
data_layer/attention ─┼─► steering.kernel.evaluate_steering()
|
||||
signals/engine (init.) ─┘ │
|
||||
├─ resolve_binding (primary + modifier)
|
||||
├─ resolve_horizon
|
||||
├─ next_work (Strategy via binding)
|
||||
└─ attention (method-aware, horizon-scoped)
|
||||
```
|
||||
|
||||
**Stop-the-line:** Keine neue Steuerungsheuristik außerhalb `backend/steering/kernel/` und registrierten Method-Strategies, die **nur** vom Kernel aufgerufen werden.
|
||||
|
||||
---
|
||||
|
||||
## 4. Abnahme (DoD)
|
||||
|
||||
1. A2 Happy Path Spec §9: Next im Gate-Horizont, blockiert → Attention, kein Fake-Next
|
||||
2. Agile: Primary ohne Sprint; Sprint aktiv → `work_cycle_ready` ∩ Horizont
|
||||
3. pytest `test_ap_kernel_spine.py` + `test_ap22c_*` grün
|
||||
4. Kein Initiative-Snapshot-Pfad umgeht `evaluate_steering()`
|
||||
|
||||
---
|
||||
|
||||
## 5. Migration
|
||||
|
||||
| Alt | Neu |
|
||||
|-----|-----|
|
||||
| `signals/engine.evaluate(..., initiative_id=…)` | delegiert an Kernel |
|
||||
| Composition-If in `engine._resolve_next_action_strategy_key` | `kernel/binding.py` |
|
||||
| Horizont in einzelnen Strategies | `kernel/horizon.py` + Strategies konsumieren Scope |
|
||||
|
||||
Strategies bleiben **Plugin-Lieferanten** — werden nicht dupliziert, aber nur über Kernel geroutet.
|
||||
|
||||
---
|
||||
|
||||
## 6. Nächste Phasen (nach Spine ✓)
|
||||
|
||||
1. Zweite Methode durch dieselbe Spine (`continuous_product`)
|
||||
2. `checklist_flow`, `care_navigation` als Plugins
|
||||
3. Event-Ingress für hybrid/event-Methoden
|
||||
4. Lifecycle-Schleife schrittweise (nicht Big Bang)
|
||||
|
||||
### Phase v0.1.1 (2026-07-26) ✓
|
||||
|
||||
- `slot_map` in `MethodDefinition` (Spec-D D5) für `sequential_dependency`, `generic_operating`
|
||||
- `evaluate_steering()` liefert `lifecycle`: `current_state`, `slot_map`, `active_slots`
|
||||
- Snapshot-Feld `steering_kernel.lifecycle`
|
||||
|
||||
---
|
||||
|
||||
*Bei Erweiterung: v0.2 Event-Pfad + Slot-Provider — v0.2 + PO-Freigabe.*
|
||||
|
|
@ -86,6 +86,7 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
|||
| Initiative Steering Snapshot | ✓ | Actions + linked; Archetyp/Guidance AP2.0c |
|
||||
| Operating Context API | ✓ | AP2.3a: `GET …/operating-context`, `ui_profile_json` Migration 027 |
|
||||
| Archetyp-/Methoden-Plugin-Architektur | ✓ | AP2.3 A–F + AP2.4; Operating Context, Slice-Loader, Route Gating |
|
||||
| **Steering Kernel Spine** | ◐ | v0.1.1: evaluate_steering + slot_map (A2); Event-Ingress Stub |
|
||||
| Steuerungselement-Registry | ✓ | AP2.4: `steering/elements/`, FE `steeringElementRegistry`, Methoden-Vertrag |
|
||||
| operating_phase | ✗ | entfernt AP1.2 |
|
||||
| signals (Snapshot) | ✓ | `snapshot_signals.py` |
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user