Kairo-Jinkendo/backend/services/operating_context.py
Lars 753f178b0c
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Failing after 3m23s
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 3s
Test Suite / compose-smoke (push) Has been skipped
docs: Archetyp-Vollspecs und Methodenkern vorlaeufig freigeben.
Decision-Lock, Spec-D-Methoden und C1-Massstab-Vollspecs als Zielbild; Steering-Registry/Compat und Horizon-Tests an die Normierung anbinden. Implementierungsausreichendheit bleibt bewusst offen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 15:02:04 +02:00

199 lines
6.2 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.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 _resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
db_profile = _load_ui_profile_from_db(archetype_key)
if db_profile:
return db_profile
seed_profile = get_ui_profile_json(archetype_key)
if seed_profile:
return seed_profile
return resolve_ui_profile(archetype_key)
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 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)
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": 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": (
"agile_iteration"
if _resolve_active_agile_composition(
tenant_id=tenant_id,
initiative_id=initiative_id,
primary_method_key=method_key,
)
else None
),
}
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