Files
govoplan-records/src/govoplan_records/backend/manifest.py
T
zemion 0363e9f589
Module Package Release / publish-packages (push) Successful in 12s
Release Records package v0.1.19
2026-08-06 20:14:14 +02:00

607 lines
22 KiB
Python

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