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.information_governance import ( InformationGovernanceDimension, ModuleInformationGovernance, ) 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, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.records import ( CAPABILITY_RECORDS_FILING, record_archive_capability, ) from govoplan_core.core.search import SearchSourceProviderRegistration from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_records.backend.db import models as record_models from govoplan_records.backend.search_source import create_records_search_source from govoplan_records.backend.service import SqlRecordRegistry from govoplan_records.backend.archive import ( SIMULATION_PROVIDER_ID, SimulatedRecordArchiveProvider, ) MODULE_ID = "records" MODULE_NAME = "Records" MODULE_VERSION = "0.1.19" READ_SCOPE = "records:workspace:read" WRITE_SCOPE = "records:workspace:write" ADMIN_SCOPE = "records:workspace:admin" OPTIONAL_DEPENDENCIES = ( "files", "cases", "forms_runtime", "decisions", "campaigns", "postbox", "reporting", "dms", "docs", "policy", "approvals", "audit", "transparency", "search", ) 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="Records", level="tenant", module_id=module_id, resource=resource, action=action, ) def _router(context: ModuleContext): from govoplan_records.backend.router import create_router return create_router(context.registry) def _records_registry(context: ModuleContext) -> SqlRecordRegistry: return SqlRecordRegistry(context.registry) def _simulated_archive_provider( context: ModuleContext, ) -> SimulatedRecordArchiveProvider: del context return SimulatedRecordArchiveProvider() def _tenant_summary(session, tenant_id: str) -> dict[str, int]: records = ( session.query(record_models.RecordIdentity) .filter(record_models.RecordIdentity.tenant_id == tenant_id) .count() ) open_records = ( session.query(record_models.RecordRevision) .filter( record_models.RecordRevision.tenant_id == tenant_id, record_models.RecordRevision.superseded_at.is_(None), record_models.RecordRevision.state == "open", ) .count() ) active_holds = ( session.query(record_models.RecordHoldRevision) .filter( record_models.RecordHoldRevision.tenant_id == tenant_id, record_models.RecordHoldRevision.superseded_at.is_(None), record_models.RecordHoldRevision.status == "active", ) .count() ) pending_dispositions = ( session.query(record_models.RecordDispositionRevision) .filter( record_models.RecordDispositionRevision.tenant_id == tenant_id, record_models.RecordDispositionRevision.superseded_at.is_(None), record_models.RecordDispositionRevision.status.in_( ("review_pending", "review_unavailable") ), ) .count() ) return { "records": records, "open_records": open_records, "active_holds": active_holds, "pending_dispositions": pending_dispositions, } PERMISSIONS = ( _permission( READ_SCOPE, "View records workspace", "Read currently authorized records, contents, chronology, and file-plan context.", ), _permission( WRITE_SCOPE, "Manage records workspace", "Create and revise records, create volumes, and file exact source revisions.", ), _permission( ADMIN_SCOPE, "Administer records workspace", "Version file-plan nodes and record classes and administer Records configuration.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="records_manager", name="Records manager", description="Create, revise, structure, and file content into records.", permissions=(READ_SCOPE, WRITE_SCOPE), ), RoleTemplate( slug="records_viewer", name="Records viewer", description="Read records and their governed chronology.", permissions=(READ_SCOPE,), ), RoleTemplate( slug="records_administrator", name="Records administrator", description="Configure file plans and record classes and manage records.", permissions=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE), ), ) DOCUMENTATION = ( DocumentationTopic( id="records.workspace", title="eAkte workspace", summary="Create and browse institutional records, their exact filed items, and chronology.", body=( "Records owns the stable record identity, file-plan classification, immutable revisions, " "volumes, filing decisions, and chronology. Files and other source modules continue to own " "their content. Filing resolves and preserves an exact source revision only after the source " "module confirms current access. The titlebar temporal selection changes valid and recorded " "time while current authorization always remains in force." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "records_manager", "operator", "module_admin", "auditor"), order=100, related_modules=OPTIONAL_DEPENDENCIES, links=( DocumentationLink( label="eAkte architecture", href="govoplan-records/docs/EAKTE_ARCHITECTURE.md", kind="repository", ), ), translations={ "de": { "title": "eAkte-Arbeitsbereich", "summary": "Institutionelle Akten, exakt veraktete Objekte und die Chronologie anlegen und einsehen.", "body": ( "Records verwaltet die stabile Aktenidentität, Aktenplanklassifikation, unveränderliche " "Revisionen, Bände, Veraktungsentscheidungen und die Chronologie. Dateien und andere " "Quellmodule bleiben Eigentümer ihrer Inhalte. Bei der Veraktung wird erst nach aktueller " "Zugriffsprüfung durch das Quellmodul eine exakte Quellrevision festgehalten. Die temporale " "Auswahl in der Titelleiste ändert Gültigkeits- und Erfassungszeit; die aktuelle Berechtigung " "gilt stets weiter." ), } }, metadata={ "help_contexts": [ "records.workspace", "records.file-plan", "records.record-list", "records.record-detail", "records.record-items", "records.chronology", "records.action.create", "records.action.edit", "records.field.record-number", "records.field.state", "records.field.title", "records.field.class", "records.field.classification", "records.field.description", "records.field.change-reason", ], }, ), DocumentationTopic( id="records.filing", title="Exact record filing", summary="File immutable Files or Cases revisions through a provider-neutral capability.", body=( "Every filing requires a record, purpose, filing reason, idempotency key, and exact source " "revision. Records stores source identity, authority mode, digest and content metadata where " "available, represented valid time, source recorded time, filing actor and capacity, and an " "immutable chronology entry. A repeated idempotency key replays only the identical request." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "records_manager", "operator", "module_admin", "auditor"), order=110, related_modules=("files", "cases", "policy", "audit"), links=( DocumentationLink( label="Records domain boundary", href="govoplan-records/docs/RECORDS_DOMAIN_BOUNDARY.md", kind="repository", ), ), translations={ "de": { "title": "Exakte Veraktung", "summary": "Unveränderliche Datei- oder Vorgangsrevisionen über eine anbieterneutrale Schnittstelle verakten.", "body": ( "Jede Veraktung benötigt eine Akte, einen Zweck, eine Veraktungsbegründung, einen " "Idempotenzschlüssel und eine exakte Quellrevision. Records speichert Quellidentität, " "Autoritätsmodus, soweit verfügbar Prüfsumme und Inhaltsmetadaten, Gültigkeits- und " "Erfassungszeit der Quelle, handelnde Person und Funktion sowie einen unveränderlichen " "Chronologieeintrag. Ein wiederholter Idempotenzschlüssel gibt nur dieselbe Anfrage erneut aus." ), } }, metadata={ "help_contexts": [ "records.action.file", "records.field.source-module", "records.field.source-object", "records.field.source-revision", "records.field.purpose", "records.field.filing-reason", ], }, ), DocumentationTopic( id="records.lifecycle", title="Governed records lifecycle", summary="Close, retain, hold, appraise, approve, and package records with durable evidence.", body=( "Closing a record applies the versioned record-class retention rule. Reopening preserves the " "previous schedule unless an administrator deliberately restarts it. Effective-dated holds block " "disposition and transfer. Appraisal selects retain, transfer, destroy, or reclassify; a disposition " "binds the exact evidence digest and requires independent approval through Approvals. Approval only " "changes lifecycle state. Destruction remains pending and transfer simulation never claims custody. " "Every API mutation is fenced and recorded in the Core recovery ledger." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "records_manager", "operator", "module_admin", "auditor"), order=120, related_modules=("policy", "approvals", "audit", "dms"), translations={ "de": { "title": "Gesteuerter Aktenlebenszyklus", "summary": "Akten mit dauerhaftem Nachweis abschließen, aufbewahren, sperren, bewerten, freigeben und paketieren.", "body": ( "Beim Abschluss einer Akte wird die versionierte Aufbewahrungsregel der Aktenklasse angewendet. " "Eine Wiedereröffnung bewahrt den bisherigen Zeitplan, sofern ein Administrator ihn nicht " "bewusst neu startet. Gültigkeitsbezogene Sperren blockieren Aussonderung und Übergabe. Die " "Bewertung wählt Aufbewahrung, Übergabe, Vernichtung oder Neuklassifikation; die Aussonderung " "bindet den exakten Nachweis und benötigt eine unabhängige Freigabe durch Approvals. Eine " "Freigabe ändert nur den Lebenszyklusstatus. Vernichtung bleibt vorgemerkt, und eine Simulation " "behauptet keine Archivverwahrung. Jede API-Änderung wird im Recovery-Ledger abgesichert." ), } }, metadata={ "help_contexts": [ "records.lifecycle", "records.lifecycle.volume", "records.lifecycle.close", "records.lifecycle.reopen", "records.lifecycle.appraise", "records.lifecycle.hold", "records.lifecycle.release-hold", "records.lifecycle.disposition", "records.lifecycle.finalize", "records.lifecycle.prepare-transfer", "records.lifecycle.dispatch-transfer", "records.lifecycle.recovery", "records.catalog.admin", "records.field.volume", "records.field.volume-label", "records.field.retention-trigger", "records.field.disposition", "records.field.hold-authority", "records.field.lifecycle-reason", "records.field.archive-provider", "records.field.archive-profile", ], }, ), ) 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, ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, route_factory=_router, nav_items=( NavItem( path="/records", label="Records", icon="archive", required_any=(READ_SCOPE,), order=47, surface_id="records.navigation", ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/records-webui", routes=( FrontendRoute( path="/records", component="RecordsPage", required_any=(READ_SCOPE,), order=47, surface_id="records.workspace", ), ), nav_items=( NavItem( path="/records", label="Records", icon="archive", required_any=(READ_SCOPE,), order=47, surface_id="records.navigation", ), ), view_surfaces=( ViewSurface( id="records.workspace.file-plan", module_id=MODULE_ID, kind="section", label="File plan", parent_id="records.workspace", order=10, ), ViewSurface( id="records.workspace.list", module_id=MODULE_ID, kind="section", label="Record list", parent_id="records.workspace", order=20, ), ViewSurface( id="records.workspace.detail", module_id=MODULE_ID, kind="section", label="Record detail", parent_id="records.workspace", order=30, ), ViewSurface( id="records.workspace.file", module_id=MODULE_ID, kind="action", label="File source revision", parent_id="records.workspace.detail", order=40, ), ), ), provides_interfaces=( ModuleInterfaceProvider(name="records.registry", version="1.0.0"), ModuleInterfaceProvider(name="records.filing", version="1.0.0"), ModuleInterfaceProvider(name="records.archive", version="1.0.0"), ), capability_factories={ CAPABILITY_RECORDS_FILING: _records_registry, record_archive_capability(SIMULATION_PROVIDER_ID): _simulated_archive_provider, }, capability_documentation={ CAPABILITY_RECORDS_FILING: CapabilityDocumentation( label="Record filing", summary="Resolves authorized exact source revisions and files immutable record items.", contract_version="1.0.0", ), record_archive_capability(SIMULATION_PROVIDER_ID): CapabilityDocumentation( label="Record archive simulation", summary=( "Validates archive-neutral package and receipt handling without transferring custody." ), contract_version="1.0.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( record_models.RecordChronologyEntry, record_models.RecordTransferPackageRevision, record_models.RecordDispositionRevision, record_models.RecordHoldRevision, record_models.RecordItem, record_models.RecordVolumeRevision, record_models.RecordRevision, record_models.RecordIdentity, record_models.RecordClassRevision, record_models.RecordFilePlanRevision, label="Records", ), retirement_notes=( "Destructive retirement requires a database snapshot and removes record identities, " "file plans, exact filing references, and chronology. Source content remains provider-owned." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( record_models.RecordIdentity, record_models.RecordRevision, record_models.RecordItem, record_models.RecordChronologyEntry, record_models.RecordHoldRevision, record_models.RecordDispositionRevision, record_models.RecordTransferPackageRevision, record_models.RecordClassRevision, record_models.RecordFilePlanRevision, label="Records", ), ), tenant_summary_providers=(_tenant_summary,), search_sources=( SearchSourceProviderRegistration( id="records.objects", factory=create_records_search_source ), ), documentation=DOCUMENTATION, information_governance=ModuleInformationGovernance( temporal_browsing=InformationGovernanceDimension( adoption="enforced", object_types=( "record", "record_volume", "record_item", "record_class", "file_plan_node", "record_hold", "record_disposition", "record_transfer_package", ), evidence=( "src/govoplan_records/backend/service.py", "tests/test_records.py", ), ), purpose_aware_access=InformationGovernanceDimension( adoption="partial", object_types=("record", "record_item"), evidence=("src/govoplan_records/backend/service.py",), limitation=( "Purposes are mandatory and preserved for record operations, but Policy-backed " "object-level purpose constraints and restricted-record grants are not implemented yet." ), ), retention=InformationGovernanceDimension( adoption="enforced", object_types=( "record", "record_class", "record_hold", "record_disposition", ), evidence=( "src/govoplan_records/backend/service.py", "tests/test_records.py", ), ), institutional_context=InformationGovernanceDimension( adoption="enforced", object_types=( "record", "record_item", "record_event", "record_class", "file_plan_node", ), evidence=( "src/govoplan_records/backend/db/models.py", "src/govoplan_records/backend/service.py", "tests/test_records.py", ), ), ), architecture=declared_module_architecture( layer="content_records_evidence", kind="domain", maturity="vertical_slice", documentation_ref="docs/EAKTE_ARCHITECTURE.md", test_ref="tests/test_records.py", known_limits=( "Restricted object grants are tracked separately.", "Archive packaging is native, but real target conformance and destructive effects remain external work.", ), supported_authority_modes=( "native_authoritative", "external_authoritative", "external_mirror", "governed_sync", "governance_overlay", "linked_reference", ), owned_concepts=( "record", "record class", "file plan", "record volume", "record item", "filing decision", "record chronology", "record hold", "record appraisal", "record disposition", "record transfer package", ), non_owned_concepts=( "file content", "source object", "case lifecycle", "workflow execution", "generic policy", "audit event", "archive preservation provider", ), reference_packages=( "product.service-to-decision", "product.monthly-data-operations", ), migration_docs=("docs/EAKTE_ARCHITECTURE.md",), recovery_docs=("docs/EAKTE_ARCHITECTURE.md",), security_docs=("docs/EAKTE_ARCHITECTURE.md",), operations_docs=("docs/EAKTE_ARCHITECTURE.md",), ), ) def get_manifest() -> ModuleManifest: return manifest