from __future__ import annotations from pathlib import Path from govoplan_core.core.access import ( CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER, ) from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( DocumentationLink, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleManifest, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.search import ( CAPABILITY_SEARCH_INDEX_WRITER, SearchProviderRegistration, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_search.backend.db import models as search_models MODULE_ID = "search" MODULE_NAME = "Search" MODULE_VERSION = "0.1.14" READ_SCOPE = "search:result:read" INDEX_SCOPE = "search:index:write" ADMIN_SCOPE = "search:index:admin" def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Search", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( READ_SCOPE, "Search available content", "Search content the current principal is authorized to read.", ), _permission( INDEX_SCOPE, "Write search index entries", "Publish and remove module-owned entries in the search index.", ), _permission( ADMIN_SCOPE, "Administer search index", "Inspect providers and rebuild tenant search indexes.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="search_user", name="Search user", description="Search content available to the current account.", permissions=(READ_SCOPE,), default_authenticated=True, ), RoleTemplate( slug="search_manager", name="Search manager", description="Search content and administer indexing.", permissions=(READ_SCOPE, INDEX_SCOPE, ADMIN_SCOPE), ), ) def _service(context: ModuleContext): from govoplan_search.backend.service import SearchIndexService return SearchIndexService(context.registry) def _router(_context: ModuleContext): from govoplan_search.backend.router import router return router manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, optional_dependencies=( "access", "views", "connectors", "wiki", "projects", "tickets", "cases", ), required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, ), provides_interfaces=( ModuleInterfaceProvider(name="search.provider", version="1.0.0"), ModuleInterfaceProvider(name="search.index_writer", version="1.1.0"), ModuleInterfaceProvider(name="search.source", version="1.0.0"), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, route_factory=_router, frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/search-webui", routes=( FrontendRoute( path="/search", component="SearchPage", required_any=(READ_SCOPE,), order=12, ), ), view_surfaces=( ViewSurface( id="search.global", module_id=MODULE_ID, kind="selector", label="Global search", description="Search entry in the title bar.", order=10, ), ViewSurface( id="search.results", module_id=MODULE_ID, kind="route", label="Search results", order=20, ), ViewSurface( id="search.admin.index", module_id=MODULE_ID, kind="section", label="Search index administration", order=30, ), ), ), migration_spec=MigrationSpec( module_id=MODULE_ID, metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( search_models.SearchIndexChangeQueue, search_models.SearchIndexAclToken, search_models.SearchIndexDocument, search_models.SearchIndexState, label="Search", ), retirement_notes=( "Destructive retirement removes the derived search index. " "Source module data remains authoritative." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( search_models.SearchIndexChangeQueue, search_models.SearchIndexDocument, search_models.SearchIndexState, label="Search index", ), ), capability_factories={ CAPABILITY_SEARCH_INDEX_WRITER: _service, }, search_providers=( SearchProviderRegistration( id="search.index", factory=_service, order=10, ), ), documentation=( DocumentationTopic( id="search.global-and-contextual", title="Global and contextual search", summary=( "Search authorized native and connected objects from one " "permission-aware interface." ), body=( "Search works with the built-in database index and can aggregate " "optional providers. Source modules announce searchable types, " "context scopes, and ACL-aware index entries. External engines " "remain optional adapters. F3 or the title-bar field opens the " "keyboard-navigable search overlay; filters never broaden the " "current principal's source permissions. Provider failures are " "shown as partial diagnostics without discarding safe results." " Search administrators can inspect native source coverage, " "process queued changes, reconcile enabled modules, and run " "bounded source rebuilds from Administration. Quarantined " "changes remain visible until repaired and reconciled." ), layer="available", documentation_types=("admin", "user"), audience=("administrator", "user"), related_modules=("connectors", "views"), links=( DocumentationLink( label="Search interface pattern audit", href="govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md", kind="repository", ), ), order=12, ), ), architecture=declared_module_architecture( layer="governance_accountability", kind="runtime", maturity="vertical_slice", documentation_ref="README.md", test_ref="tests/test_postgres_search.py", known_limits=("The built-in PostgreSQL index is implemented; optional OpenSearch target evidence is not.",), supported_authority_modes=("external_mirror",), owned_concepts=("derived search index", "search ACL projection", "index change queue"), non_owned_concepts=("source object", "source authorization", "external search engine"), recovery_docs=("README.md",), security_docs=("README.md",), operations_docs=("README.md",), ), ) def get_manifest() -> ModuleManifest: return manifest __all__ = [ "ADMIN_SCOPE", "INDEX_SCOPE", "MODULE_ID", "MODULE_VERSION", "READ_SCOPE", "get_manifest", "manifest", ]