feat(records): govern lifecycle recovery and transfer evidence

This commit is contained in:
2026-08-06 05:36:18 +02:00
parent f316523a1d
commit df51bb1787
22 changed files with 4954 additions and 66 deletions
+122 -23
View File
@@ -29,13 +29,20 @@ from govoplan_core.core.modules import (
RoleTemplate,
)
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.records import CAPABILITY_RECORDS_FILING
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"
@@ -55,6 +62,7 @@ OPTIONAL_DEPENDENCIES = (
"dms",
"docs",
"policy",
"approvals",
"audit",
"transparency",
"search",
@@ -85,6 +93,13 @@ 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)
@@ -100,7 +115,32 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
)
.count()
)
return {"records": records, "open_records": open_records}
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 = (
@@ -247,14 +287,16 @@ DOCUMENTATION = (
},
),
DocumentationTopic(
id="records.lifecycle-limitations",
title="Records lifecycle limitations",
summary="Identifies lifecycle controls intentionally deferred beyond the native kernel.",
id="records.lifecycle",
title="Governed records lifecycle",
summary="Close, retain, hold, appraise, approve, and package records with durable evidence.",
body=(
"The current vertical supports planned and open records. Restricted object grants, closure, "
"retention calculation, holds, appraisal, disposition, transfer, destruction, and external "
"archive effects are separate governed work packages. No destructive effect is implied by "
"enabling Records."
"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"),
@@ -263,17 +305,44 @@ DOCUMENTATION = (
related_modules=("policy", "approvals", "audit", "dms"),
translations={
"de": {
"title": "Grenzen des Aktenlebenszyklus",
"summary": "Kennzeichnet bewusst nach dem nativen Kern umzusetzende Lebenszyklussteuerungen.",
"title": "Gesteuerter Aktenlebenszyklus",
"summary": "Akten mit dauerhaftem Nachweis abschließen, aufbewahren, sperren, bewerten, freigeben und paketieren.",
"body": (
"Der aktuelle Stand unterstützt geplante und offene Akten. Objektbezogene Freigaben, "
"Abschluss, Aufbewahrungsberechnung, Sperren, Bewertung, Aussonderung, Übergabe, Vernichtung "
"und externe Archiveffekte sind getrennte gesteuerte Arbeitspakete. Die Aktivierung von "
"Records löst keine vernichtende Wirkung aus."
"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={"known_limit": True},
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",
],
},
),
)
@@ -361,14 +430,25 @@ manifest = ModuleManifest(
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},
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,
@@ -377,6 +457,9 @@ manifest = ModuleManifest(
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,
@@ -396,6 +479,9 @@ manifest = ModuleManifest(
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",
@@ -417,6 +503,9 @@ manifest = ModuleManifest(
"record_item",
"record_class",
"file_plan_node",
"record_hold",
"record_disposition",
"record_transfer_package",
),
evidence=(
"src/govoplan_records/backend/service.py",
@@ -433,10 +522,16 @@ manifest = ModuleManifest(
),
),
retention=InformationGovernanceDimension(
adoption="contract_only",
limitation=(
"Record classes preserve retention inputs; closure, holds, calculation, appraisal, "
"and disposition are tracked in Records #5."
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(
@@ -462,8 +557,8 @@ manifest = ModuleManifest(
documentation_ref="docs/EAKTE_ARCHITECTURE.md",
test_ref="tests/test_records.py",
known_limits=(
"Restricted object grants and lifecycle stages after open are tracked separately.",
"Archive transfer and destructive effects are not part of the native kernel.",
"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",
@@ -481,6 +576,10 @@ manifest = ModuleManifest(
"record item",
"filing decision",
"record chronology",
"record hold",
"record appraisal",
"record disposition",
"record transfer package",
),
non_owned_concepts=(
"file content",