"""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