feat: implement committee decision workspace

This commit is contained in:
2026-08-01 17:48:25 +02:00
parent 232329ac52
commit 758b59ca03
28 changed files with 4909 additions and 32 deletions
+297 -15
View File
@@ -1,23 +1,149 @@
from __future__ import annotations
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.modules import DocumentationLink, DocumentationTopic, ModuleManifest, PermissionDefinition, RoleTemplate
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.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",
"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.",
"External and secret ballots are delegated to provider adapters. Committee stores only verified aggregates, provider receipts, hashes, and evidence rather than individual ballots.",
"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)
@@ -34,9 +160,31 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
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(
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 = (
@@ -44,7 +192,7 @@ ROLE_TEMPLATES = (
slug="committee_manager",
name="Committee manager",
description="Manage committee records and workflow state.",
permissions=(READ_SCOPE, WRITE_SCOPE),
permissions=(READ_SCOPE, WRITE_SCOPE, BALLOT_SCOPE, PROTECTED_READ_SCOPE),
),
RoleTemplate(
slug="committee_viewer",
@@ -58,15 +206,20 @@ 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, decisions, voting, and follow-up tasks.",
summary="Committee, board, council, and senate workflows for meetings, agendas, minutes, deliberation, voting, formal decision references, and follow-up tasks.",
body=(
"This repository is currently a platform module seed. It registers the domain boundary, "
"permission surface, role templates, and documentation metadata before runtime APIs, "
"database models, migrations, and WebUI routes are introduced."
"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",),
audience=("operator", "module_admin", "product_owner"),
documentation_types=("admin", "user"),
audience=("user", "operator", "module_admin", "product_owner"),
order=100,
related_modules=OPTIONAL_DEPENDENCIES,
links=(
@@ -78,8 +231,19 @@ DOCUMENTATION = (
),
metadata={
"seed": True,
"domain_objects": ['committee bodies', 'meeting agendas', 'minutes', 'decision records', 'votes', 'follow-up assignments'],
"first_slice": "Define committee body, meeting, agenda item, decision, vote, minute, and follow-up task references.",
"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"
],
},
),
)
@@ -90,10 +254,128 @@ manifest = ModuleManifest(
version=MODULE_VERSION,
dependencies=("access",),
optional_dependencies=OPTIONAL_DEPENDENCIES,
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
),
optional_capabilities=(
CAPABILITY_MANDATE_RESOLVER,
CAPABILITY_DECISION_REGISTRY,
),
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),
),
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,
)