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
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>
249 lines
8.3 KiB
Python
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
|