  # #region rbac_permission_catalog [C:2] [TYPE Module] [SEMANTICS rbac, permission, discovery, route, sync]
  # @defgroup Services Module group.
  #
  # @BRIEF Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database.
  
> from collections.abc import Iterable
> from functools import lru_cache
> from pathlib import Path
> import re
  
> from sqlalchemy.orm import Session
  
> from ..core.logger import belief_scope, logger
> from ..models.auth import Permission
  
  # #region HAS_PERMISSION_PATTERN [TYPE Constant]
  # @ingroup Services
  # @BRIEF Regex pattern for extracting has_permission("resource", "ACTION") declarations.
> HAS_PERMISSION_PATTERN = re.compile(
>     r"""has_permission\(\s*['"]([^'"]+)['"]\s*,\s*['"]([A-Z]+)['"]\s*\)"""
> )
  # #endregion HAS_PERMISSION_PATTERN
  
  # #region ROUTES_DIR [TYPE Constant]
  # @ingroup Services
  # @BRIEF Absolute directory path where API route RBAC declarations are defined.
> ROUTES_DIR = Path(__file__).resolve().parent.parent / "api" / "routes"
  # #endregion ROUTES_DIR
  
  
  # #region _iter_route_files [C:4] [TYPE Function]
  # @BRIEF Iterates API route files that may contain RBAC declarations.
  # @PRE  ROUTES_DIR points to backend/src/api/routes.
  # @POST  Yields Python files excluding test and cache directories.
> def _iter_route_files() -> Iterable[Path]:
>     with belief_scope("rbac_permission_catalog._iter_route_files"):
>         if not ROUTES_DIR.exists():
>             return []
  
>         files = []
>         for file_path in ROUTES_DIR.rglob("*.py"):
>             path_parts = set(file_path.parts)
>             if "__tests__" in path_parts or "__pycache__" in path_parts:
>                 continue
>             files.append(file_path)
>         return files
  # #endregion _iter_route_files
  
  
  # #region _discover_route_permissions [C:4] [TYPE Function]
  # @BRIEF Extracts explicit has_permission declarations from API route source code.
  # @PRE  Route files are readable UTF-8 text files.
  # @POST  Returns unique set of (resource, action) pairs declared in route guards.
> def _discover_route_permissions() -> set[tuple[str, str]]:
>     with belief_scope("rbac_permission_catalog._discover_route_permissions"):
>         discovered: set[tuple[str, str]] = set()
>         for route_file in _iter_route_files():
>             try:
>                 source = route_file.read_text(encoding="utf-8")
!             except OSError as read_error:
!                 logger.explore(
!                     "Failed to read route file",
!                     extra={"src": "rbac_permission_catalog", "payload": {"file": str(route_file)}, "error": str(read_error)},
!                 )
!                 continue
  
>             for resource, action in HAS_PERMISSION_PATTERN.findall(source):
>                 normalized_resource = str(resource or "").strip()
>                 normalized_action = str(action or "").strip().upper()
>                 if normalized_resource and normalized_action:
>                     discovered.add((normalized_resource, normalized_action))
>         return discovered
  # #endregion _discover_route_permissions
  
  
  # #region _discover_route_permissions_cached [C:4] [TYPE Function]
  # @BRIEF Cache route permission discovery because route source files are static during normal runtime.
  # @PRE  None.
  # @POST  Returns stable discovered route permission pairs without repeated filesystem scans.
> @lru_cache(maxsize=1)
> def _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:
>     with belief_scope("rbac_permission_catalog._discover_route_permissions_cached"):
>         return tuple(sorted(_discover_route_permissions()))
  # #endregion _discover_route_permissions_cached
  
  
  # #region _discover_plugin_execute_permissions [C:4] [TYPE Function]
  # @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.
  # @PRE  plugin_loader is optional and may expose get_all_plugin_configs.
  # @POST  Returns unique plugin EXECUTE permissions if loader is available.
> def _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, str]]:
!     with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions"):
!         discovered: set[tuple[str, str]] = set()
!         if plugin_loader is None:
!             return discovered
  
!         try:
!             plugin_configs = plugin_loader.get_all_plugin_configs()
!         except Exception as plugin_error:
!             logger.explore(
!                 "Failed to read plugin configs for RBAC discovery",
!                 extra={"src": "rbac_permission_catalog", "error": str(plugin_error)},
!             )
!             return discovered
  
!         for plugin_config in plugin_configs:
!             plugin_id = str(getattr(plugin_config, "id", "") or "").strip()
!             if plugin_id:
!                 discovered.add((f"plugin:{plugin_id}", "EXECUTE"))
!         return discovered
  # #endregion _discover_plugin_execute_permissions
  
  
  # #region _discover_plugin_execute_permissions_cached [C:4] [TYPE Function]
  # @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.
  # @PRE  plugin_ids is a deterministic tuple of plugin ids.
  # @POST  Returns stable permission tuple without repeated plugin catalog expansion.
> @lru_cache(maxsize=8)
> def _discover_plugin_execute_permissions_cached(
>     plugin_ids: tuple[str, ...],
> ) -> tuple[tuple[str, str], ...]:
>     with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions_cached"):
>         return tuple((f"plugin:{plugin_id}", "EXECUTE") for plugin_id in plugin_ids)
  # #endregion _discover_plugin_execute_permissions_cached
  
  
  # #region discover_declared_permissions [C:4] [TYPE Function]
  # @ingroup Services
  # @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry.
  # @PRE  plugin_loader may be provided for dynamic task plugin permission discovery.
  # @POST  Returns union of route-declared and dynamic plugin EXECUTE permissions.
> def discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:
>     with belief_scope("rbac_permission_catalog.discover_declared_permissions"):
>         permissions = set(_discover_route_permissions_cached())
>         plugin_ids = tuple(
>             sorted(
>                 {
>                     str(getattr(plugin_config, "id", "") or "").strip()
>                     for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])
>                     if str(getattr(plugin_config, "id", "") or "").strip()
>                 }
>             )
>         )
>         permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))
>         return permissions
  # #endregion discover_declared_permissions
  
  
  # #region sync_permission_catalog [C:4] [TYPE Function]
  # @ingroup Services
  # @BRIEF Persists missing RBAC permission pairs into auth database.
  # @PRE  db is a valid SQLAlchemy session bound to auth database.
  # @PRE  declared_permissions is an iterable of (resource, action) tuples.
  # @POST  Missing permissions are inserted; existing permissions remain untouched.
  # @SIDE_EFFECT  Commits auth database transaction when new permissions are added.
> def sync_permission_catalog(
>     db: Session,
>     declared_permissions: Iterable[tuple[str, str]],
> ) -> int:
>     with belief_scope("rbac_permission_catalog.sync_permission_catalog"):
>         normalized_declared: set[tuple[str, str]] = set()
>         for resource, action in declared_permissions:
>             normalized_resource = str(resource or "").strip()
>             normalized_action = str(action or "").strip().upper()
>             if normalized_resource and normalized_action:
>                 normalized_declared.add((normalized_resource, normalized_action))
  
>         existing_permissions = db.query(Permission).all()
>         existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}
  
>         missing_pairs = sorted(normalized_declared - existing_pairs)
>         for resource, action in missing_pairs:
>             db.add(Permission(resource=resource, action=action))
  
>         if missing_pairs:
>             db.commit()
  
>         return len(missing_pairs)
  # #endregion sync_permission_catalog
  
  # #endregion rbac_permission_catalog
