All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 3m45s
Test Suite / lint-backend (push) Successful in 2s
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 13s
test_resolve_ui_profile_merges_seed_slices_when_db_partial erwartet omCapabilities aus dataSlices wenn DB-Profil unvollstaendig ist.
219 lines
7.0 KiB
Python
219 lines
7.0 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 _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 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
|