feat(quick-access): expose effective preference provenance

This commit is contained in:
2026-08-20 02:39:41 +02:00
parent b5b629b357
commit 127b6ee5d0
8 changed files with 211 additions and 2 deletions
@@ -18,6 +18,7 @@ from govoplan_quick_access.backend.schemas import (
CatalogueToolResponse,
EffectiveCategoryResponse,
EffectiveQuickAccessResponse,
EffectiveStalePreferenceResponse,
EffectiveToolResponse,
PreferenceEntry,
ProfileResponse,
@@ -241,6 +242,7 @@ def resolve_effective(
("user", get_profile(session, scope_type="user", tenant_id=tenant_id, scope_id=account_id)),
)
diagnostics: list[str] = []
stale_preferences = _stale_preferences(profiles, catalogue)
category_states: dict[str, _EffectiveState] = {}
for category in catalogue.categories:
state = _EffectiveState(enabled=True, order=category.order)
@@ -259,6 +261,11 @@ def resolve_effective(
state.apply(entry, source=source)
category_state = category_states.get(tool.category_id)
enabled = state.enabled and bool(category_state and category_state.enabled)
availability_source = (
category_state.availability_source
if state.enabled and category_state and not category_state.enabled
else state.availability_source
)
tool_payload = tool.model_dump()
tool_payload["order"] = state.order
tools_by_category.setdefault(tool.category_id, []).append(
@@ -267,6 +274,9 @@ def resolve_effective(
enabled=enabled,
forced=state.forced,
locked_by=state.locked_by,
availability_state=_availability_state(enabled, state.forced),
availability_source=availability_source,
order_source=state.order_source,
)
)
@@ -286,12 +296,16 @@ def resolve_effective(
enabled=enabled,
forced=state.forced,
locked_by=state.locked_by,
availability_state=_availability_state(enabled, state.forced),
availability_source=state.availability_source,
order_source=state.order_source,
tools=tools,
)
)
categories.sort(key=lambda item: (item.order, item.id))
return EffectiveQuickAccessResponse(
categories=categories,
stale_preferences=stale_preferences,
diagnostics=list(dict.fromkeys(diagnostics)),
)
@@ -302,16 +316,20 @@ class _EffectiveState:
order: int
forced: bool = False
locked_by: str | None = None
availability_source: str = "module"
order_source: str = "module"
def apply(self, entry: PreferenceEntry | None, *, source: str) -> None:
if entry is None:
return
if entry.order is not None:
self.order = entry.order
self.order_source = source
if self.locked_by is not None:
return
if entry.enabled is not None:
self.enabled = entry.enabled
self.availability_source = source
if source != "user" and entry.enabled is False:
self.forced = False
self.locked_by = source
@@ -319,6 +337,38 @@ class _EffectiveState:
self.enabled = True
self.forced = True
self.locked_by = source
self.availability_source = source
def _availability_state(enabled: bool, forced: bool) -> str:
if not enabled:
return "blocked"
return "forced" if forced else "available"
def _stale_preferences(
profiles: tuple[tuple[str, QuickAccessProfile | None], ...],
catalogue: CatalogueResponse,
) -> list[EffectiveStalePreferenceResponse]:
category_ids = {item.id for item in catalogue.categories}
tool_ids = {item.id for item in catalogue.tools}
stale: list[EffectiveStalePreferenceResponse] = []
for source, profile in profiles:
if profile is None:
continue
for kind, field, known_ids in (
("category", "category_preferences", category_ids),
("tool", "tool_preferences", tool_ids),
):
values = getattr(profile, field, {})
if not isinstance(values, Mapping):
continue
stale.extend(
EffectiveStalePreferenceResponse(id=str(item_id), kind=kind, source=source)
for item_id in values
if str(item_id) not in known_ids
)
return sorted(stale, key=lambda item: (item.source, item.kind, item.id))
def _profile_entry(