Kairo-Jinkendo/backend/services/operating_context.py
Lars a5154cbd3b
Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Failing after 4m15s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
feat(steering): Kernel v0.4 Agent-Slots, Tech Debt und Review-Attention
K-Ext-4/P8 deklarative Agent-Slots; P7 tech_debt Read Model und Vokabular; Review-Attention in den Kernel verlagert.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:54:48 +02:00

249 lines
8.3 KiB
Python

"""Operating Context read model — AP2.3a / AP2.4."""
from __future__ import annotations
from typing import Any, Optional
from psycopg2.extras import RealDictCursor
from db import get_connection
from entity_archetypes.registry import (
get_ui_profile_json,
resolve_default_method_key,
resolve_ui_profile,
)
from services.initiatives import get_initiative
from services.steering_context import ensure_steering_context, get_steering_context
from steering.graph.profiles import LIGHT, graph_profile_as_dict
from steering.backlog_vocabulary import resolve_backlog_vocabulary
from steering.effective_contract import resolve_effective_steering_elements
from steering.methods.registry import (
get_method,
list_compatible_methods,
list_composable_modifiers,
method_to_dict,
ui_features_as_dict,
)
def _load_ui_profile_from_db(archetype_key: str) -> Optional[dict[str, Any]]:
conn = get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT ui_profile_json
FROM entity_archetypes
WHERE archetype_key = %s
""",
(archetype_key,),
)
row = cur.fetchone()
if not row:
return None
profile = row.get("ui_profile_json")
if isinstance(profile, dict) and profile:
return dict(profile)
return None
finally:
conn.close()
def _seed_ui_profile(archetype_key: str) -> dict[str, Any]:
seed_profile = get_ui_profile_json(archetype_key)
if seed_profile:
return seed_profile
return resolve_ui_profile(archetype_key)
def _normalize_ui_profile_capabilities(profile: dict[str, Any]) -> dict[str, Any]:
"""Spiegelt resolve_ui_profile: omCapabilities aus dataSlices falls fehlend."""
result = dict(profile)
slices = list(result.get("dataSlices") or [])
caps = list(result.get("omCapabilities") or slices)
result["dataSlices"] = slices
result["omCapabilities"] = caps
return result
def _resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
seed_profile = _seed_ui_profile(archetype_key)
db_profile = _load_ui_profile_from_db(archetype_key)
if not db_profile:
return _normalize_ui_profile_capabilities(seed_profile)
merged = {**seed_profile, **db_profile}
if not (merged.get("omCapabilities") or merged.get("dataSlices")):
seed_slices = seed_profile.get("dataSlices") or []
merged["dataSlices"] = list(seed_slices)
return _normalize_ui_profile_capabilities(merged)
def _resolve_om_capabilities(ui_profile: dict[str, Any]) -> frozenset[str]:
caps = ui_profile.get("omCapabilities") or ui_profile.get("dataSlices") or []
return frozenset(caps)
def _resolve_data_slices(
*,
om_capabilities: frozenset[str],
method_key: str,
) -> list[str]:
"""Schnittmenge Archetyp-OM ∩ Method-Slices."""
method = get_method(method_key)
if method and method.data_slices:
method_slices = set(method.data_slices)
return [s for s in om_capabilities if s in method_slices]
return list(om_capabilities)
def _method_capabilities(method_key: str) -> dict[str, Any]:
method = get_method(method_key)
if not method:
return {
"lifecycle_steps": [],
"next_action_strategy_key": "default",
}
return {
"lifecycle_steps": list(method.default_lifecycle_steps),
"next_action_strategy_key": method.next_action_strategy_key,
}
def _resolve_method_contract(method_key: str) -> dict[str, Any]:
method = get_method(method_key)
if not method:
return {
"steering_elements": [],
"ui_features": {},
"graph_profile": graph_profile_as_dict(LIGHT),
}
return {
"steering_elements": sorted(method.steering_elements),
"ui_features": ui_features_as_dict(method.ui_features),
"graph_profile": graph_profile_as_dict(method.graph_profile),
}
def _compatible_methods_payload(
archetype_key: str,
*,
om_capabilities: frozenset[str],
active_method_key: str,
) -> list[dict[str, Any]]:
default_key = resolve_default_method_key(archetype_key)
methods = list_compatible_methods(
archetype_key, om_capabilities=om_capabilities
)
return [
{
**method_to_dict(m, include_compatibility=False),
"is_default": m.key == default_key,
"is_active": m.key == active_method_key,
}
for m in methods
]
def _resolve_agent_slot_config(metadata: dict[str, Any]) -> dict[str, Any]:
"""Governance-Konfiguration für Agent-Slots — keine Prompts."""
defaults: dict[str, Any] = {
"enabled_slots": ["review.due", "recurring.due", "review.guardrail"],
"guardrail_pack": "default_v0",
"stale_debt_days": 30,
}
custom = metadata.get("agent_slots") or metadata.get("agent_slot_config") or {}
if isinstance(custom, dict):
return {**defaults, **custom}
return defaults
def get_operating_context(
*, tenant_id: str, initiative_id: str
) -> Optional[dict[str, Any]]:
initiative = get_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
if not initiative:
return None
steering = get_steering_context(tenant_id=tenant_id, initiative_id=initiative_id)
if not steering:
steering = ensure_steering_context(
tenant_id=tenant_id, initiative_id=initiative_id
)
archetype_key = initiative["archetype_key"]
ui_profile = _resolve_ui_profile(archetype_key)
om_capabilities = _resolve_om_capabilities(ui_profile)
method_key = steering["method_key"]
metadata = steering.get("lifecycle_metadata") or {}
if isinstance(metadata, str):
metadata = {}
method_profile_key = metadata.get("method_profile_key")
data_slices = _resolve_data_slices(
om_capabilities=om_capabilities,
method_key=method_key,
)
method_contract = _resolve_method_contract(method_key)
active_composition_modifier = (
"agile_iteration"
if _resolve_active_agile_composition(
tenant_id=tenant_id,
initiative_id=initiative_id,
primary_method_key=method_key,
)
else None
)
effective_steering_elements = resolve_effective_steering_elements(
method_key,
data_slices=data_slices,
active_composition_modifier=active_composition_modifier,
)
backlog_vocabulary = resolve_backlog_vocabulary(
data_slices=data_slices,
method_key=method_key,
steering_elements=effective_steering_elements,
)
agent_slot_config = _resolve_agent_slot_config(metadata)
return {
"initiative_id": initiative_id,
"archetype_key": archetype_key,
"method_key": method_key,
"method_profile_key": method_profile_key,
"ui_profile": ui_profile,
"om_capabilities": sorted(om_capabilities),
"data_slices": data_slices,
"method_capabilities": _method_capabilities(method_key),
"steering_elements": sorted(effective_steering_elements),
"primary_steering_elements": method_contract["steering_elements"],
"ui_features": method_contract["ui_features"],
"graph_profile": method_contract["graph_profile"],
"compatible_methods": _compatible_methods_payload(
archetype_key,
om_capabilities=om_capabilities,
active_method_key=method_key,
),
"composable_modifiers": [
method_to_dict(m, include_compatibility=False)
for m in list_composable_modifiers(method_key)
],
"active_composition_modifier": active_composition_modifier,
"backlog_vocabulary": backlog_vocabulary,
"agent_slot_config": agent_slot_config,
}
def _resolve_active_agile_composition(
*,
tenant_id: str,
initiative_id: str,
primary_method_key: str,
) -> bool:
from services.work_cycle import get_active_work_cycle
from steering.methods.registry import get_method
agile = get_method("agile_iteration")
if not agile or primary_method_key not in agile.composes_with:
return False
return get_active_work_cycle(tenant_id=tenant_id, initiative_id=initiative_id) is not None