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 ( CapabilityDocumentation, DocumentationLink, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.views import ViewSurface from govoplan_core.core.institutional import ( CAPABILITY_DECISION_REGISTRY, CAPABILITY_MANDATE_RESOLVER, ) from govoplan_core.core.voting import CAPABILITY_VOTING_BALLOTS from govoplan_core.core.provider_governance import ( ModuleArchitectureDeclaration, ModuleArchitectureDocumentation, ModuleMaturityEvidence, ) from govoplan_committee.backend.decision_path import ( CAPABILITY_COMMITTEE_DECISION_PATH, CommitteeDecisionPath, ) from govoplan_committee.backend.ballots import ( CAPABILITY_COMMITTEE_BALLOT_FINALIZER, CommitteeBallotFinalizer, ) from govoplan_committee.backend.db import models as committee_models from govoplan_committee.backend.workspace import ( CAPABILITY_COMMITTEE_WORKSPACE, SqlCommitteeWorkspace, ) from govoplan_core.db.base import Base MODULE_ID = "committee" MODULE_NAME = "Committee" MODULE_VERSION = "0.1.8" READ_SCOPE = "committee:workspace:read" WRITE_SCOPE = "committee:workspace:write" BALLOT_SCOPE = "committee:ballot:finalize" ADMIN_SCOPE = "committee:workspace:admin" PROTECTED_READ_SCOPE = "committee:decision:protected_read" OPTIONAL_DEPENDENCIES = ( "calendar", "docs", "files", "mandates", "decisions", "tasks", "voting", "workflow_engine", "approvals", ) ARCHITECTURE = ModuleArchitectureDeclaration( layer="communication_participation", kind="domain", maturity="vertical_slice", evidence=( ModuleMaturityEvidence( kind="test", reference="tests/test_decision_path.py", summary="Proves effective mandate, approval, evidence, effect, and reconstruction semantics for a committee decision.", ), ModuleMaturityEvidence( kind="documentation", reference="docs/COMMITTEE_DOMAIN_BOUNDARY.md", summary="Defines the Committee/Decision/Mandate ownership boundary.", ), ), known_limits=( "The Committee WebUI covers the governed body, meeting, agenda, vote, and minute workspace; domain-specific deliberation panels can extend this surface.", "Governed ballot execution is delegated to Voting when installed. The Committee-owned provider adapter remains a 0.1 compatibility path; Committee stores only aggregate result evidence.", "Formal Decision persistence and Mandate resolution remain optional provider capabilities; the local projection is a bounded fallback.", ), owned_concepts=( "committee bodies", "meeting and agenda context", "deliberation and vote context", ), non_owned_concepts=( "formal decision lifecycle", "mandate and jurisdiction lifecycle", "generic approvals", ), reference_packages=("product.service-to-decision",), documentation=ModuleArchitectureDocumentation( security=("docs/COMMITTEE_DOMAIN_BOUNDARY.md",), operations=("docs/COMMITTEE_DOMAIN_BOUNDARY.md",), ), ) def _decision_path(context: ModuleContext) -> CommitteeDecisionPath: return CommitteeDecisionPath(context.registry) def _workspace(context: ModuleContext) -> SqlCommitteeWorkspace: del context return SqlCommitteeWorkspace() def _ballot_finalizer(context: ModuleContext) -> CommitteeBallotFinalizer: return CommitteeBallotFinalizer(context.registry) def _router(context: ModuleContext): from govoplan_committee.backend.router import configure_registry, router configure_registry(context.registry) return router def _tenant_summary(session, tenant_id: str) -> dict[str, int]: current = session.query(committee_models.CommitteeWorkspaceRevision).filter( committee_models.CommitteeWorkspaceRevision.tenant_id == tenant_id, committee_models.CommitteeWorkspaceRevision.superseded_at.is_(None), ) return { "committee_bodies": current.filter( committee_models.CommitteeWorkspaceRevision.object_kind == "body", committee_models.CommitteeWorkspaceRevision.state == "active", ).count(), "committee_meetings": current.filter( committee_models.CommitteeWorkspaceRevision.object_kind == "meeting", committee_models.CommitteeWorkspaceRevision.state.in_( ("scheduled", "open") ), ).count(), } 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="Committee", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( READ_SCOPE, "View committee workspace", "Read committee records, configuration, and workflow context.", ), _permission( WRITE_SCOPE, "Manage committee workspace", "Create and update committee records and workflow state.", ), _permission( ADMIN_SCOPE, "Administer committee workspace", "Configure committee policies, templates, and tenant-level administration.", ), _permission( BALLOT_SCOPE, "Finalize provider ballots", "Import a verifiable aggregate result from an installed external or secret ballot provider.", ), _permission( PROTECTED_READ_SCOPE, "Read protected committee decisions", "Read reasoning and protected institutional context from a Committee-owned fallback Decision projection.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="committee_manager", name="Committee manager", description="Manage committee records and workflow state.", permissions=(READ_SCOPE, WRITE_SCOPE, BALLOT_SCOPE, PROTECTED_READ_SCOPE), ), RoleTemplate( slug="committee_viewer", name="Committee viewer", description="Read committee records and workflow context.", permissions=(READ_SCOPE,), ), ) DOCUMENTATION = ( DocumentationTopic( id=f"{MODULE_ID}.module-boundary", title=f"{MODULE_NAME} module boundary", summary="Committee, board, council, and senate workflows for meetings, agendas, minutes, deliberation, voting, formal decision references, and follow-up tasks.", body=( "The persistent workspace keeps immutable bodies, meetings, agenda items, governed " "vote results, minutes, and lifecycle events. The decision path constructs a " "reconstructable formal Decision from that context, an effective Mandate resolution, " "approval, versioned legal bases, evidence, reasoning, and requested or observed " "effects. Committee does not own generic Mandate or Decision persistence; optional " "providers resolve and record those objects when installed, while a protected local " "projection preserves the bounded fallback. Provider-bound ballots are finalized " "through adapter capabilities without retaining individual ballots." ), layer="available", documentation_types=("admin", "user"), audience=("user", "operator", "module_admin", "product_owner"), order=100, related_modules=OPTIONAL_DEPENDENCIES, links=( DocumentationLink( label="Repository domain boundary", href="govoplan-committee/docs/COMMITTEE_DOMAIN_BOUNDARY.md", kind="repository", ), ), metadata={ "seed": True, "domain_objects": [ "committee bodies", "meeting agendas", "minutes", "deliberation and vote context", "formal decision references", "votes", "follow-up assignments", ], "first_slice": "Persist committee body, meeting, agenda item, governed vote result, minute, and formal Decision references.", "does_not_own": [ "generic formal decision authority, reasoning, effect, review, correction, or revocation lifecycle" ], }, ), ) manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=("access",), optional_dependencies=OPTIONAL_DEPENDENCIES, required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, ), optional_capabilities=( CAPABILITY_MANDATE_RESOLVER, CAPABILITY_DECISION_REGISTRY, CAPABILITY_VOTING_BALLOTS, ), route_factory=_router, nav_items=( NavItem( path="/committee", label="Committee", icon="gavel", required_any=(READ_SCOPE,), order=38, ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/committee-webui", routes=( FrontendRoute( path="/committee", component="CommitteePage", required_any=(READ_SCOPE,), order=38, ), ), nav_items=( NavItem( path="/committee", label="Committee", icon="gavel", required_any=(READ_SCOPE,), order=38, ), ), view_surfaces=( ViewSurface( id="committee.navigation", module_id=MODULE_ID, kind="navigation", label="Committee navigation", order=10, ), ViewSurface( id="committee.workspace", module_id=MODULE_ID, kind="route", label="Committee workspace", order=20, ), ), ), provides_interfaces=( ModuleInterfaceProvider( name="committee.decision_path", version="0.1.0", ), ModuleInterfaceProvider( name="committee.workspace", version="0.1.0", ), ModuleInterfaceProvider( name=CAPABILITY_COMMITTEE_BALLOT_FINALIZER, version="0.1.0", ), ), requires_interfaces=( ModuleInterfaceRequirement( name="mandates.resolution", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="decisions.formal_outcome", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="decisions.reconstruction", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_VOTING_BALLOTS, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ), capability_factories={ CAPABILITY_COMMITTEE_DECISION_PATH: _decision_path, CAPABILITY_COMMITTEE_WORKSPACE: _workspace, CAPABILITY_COMMITTEE_BALLOT_FINALIZER: _ballot_finalizer, }, capability_documentation={ CAPABILITY_COMMITTEE_DECISION_PATH: CapabilityDocumentation( label="Committee decision path", summary="Builds a formal, evidence-backed Decision from governed committee context.", contract_version="0.1.0", ), CAPABILITY_COMMITTEE_WORKSPACE: CapabilityDocumentation( label="Committee workspace", summary="Persists versioned bodies, meetings, agenda items, vote results, minutes, and fallback Decision projections.", contract_version="0.1.0", ), CAPABILITY_COMMITTEE_BALLOT_FINALIZER: CapabilityDocumentation( label="Committee ballot finalizer", summary="Imports aggregate result evidence from provider-neutral ballot adapters without persisting individual ballots.", contract_version="0.1.0", ), }, 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( committee_models.CommitteeDecisionProjection, committee_models.CommitteeWorkspaceEvent, committee_models.CommitteeWorkspaceRevision, label="Committee", ), retirement_notes="Destructive retirement requires a database snapshot and removes Committee workspace revisions, lifecycle events, and local Decision projections.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( committee_models.CommitteeWorkspaceRevision, committee_models.CommitteeWorkspaceEvent, committee_models.CommitteeDecisionProjection, label="Committee", ), ), tenant_summary_providers=(_tenant_summary,), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, documentation=DOCUMENTATION, architecture=ARCHITECTURE, ) def get_manifest() -> ModuleManifest: return manifest