feat(records): complete eAkte reference journey
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -12,7 +12,8 @@ authority over their objects and bytes.
|
|||||||
The first working vertical slice includes PostgreSQL/SQLite models and an
|
The first working vertical slice includes PostgreSQL/SQLite models and an
|
||||||
Alembic migration, tenant-scoped APIs, optimistic concurrency and idempotent
|
Alembic migration, tenant-scoped APIs, optimistic concurrency and idempotent
|
||||||
filing, temporal reads, search registration, a full-height Records workspace,
|
filing, temporal reads, search registration, a full-height Records workspace,
|
||||||
and provider-neutral filing from Files and Cases.
|
provider-neutral filing from Files, Cases, Forms Runtime, and Decisions, and a
|
||||||
|
governed close/retention/hold/appraisal/disposition/transfer lifecycle.
|
||||||
|
|
||||||
The `privacy.dsar.records` capability exports bounded record and lifecycle
|
The `privacy.dsar.records` capability exports bounded record and lifecycle
|
||||||
metadata from an explicit record or authoritative source linkage. Canonical
|
metadata from an explicit record or authoritative source linkage. Canonical
|
||||||
@@ -48,24 +49,38 @@ Implemented optional integrations:
|
|||||||
|
|
||||||
- files
|
- files
|
||||||
- cases
|
- cases
|
||||||
|
- forms runtime
|
||||||
|
- decisions
|
||||||
|
- approvals
|
||||||
|
- audit
|
||||||
- search
|
- search
|
||||||
|
|
||||||
Planned optional integrations include:
|
Planned optional integrations include:
|
||||||
|
|
||||||
- forms runtime and decisions
|
|
||||||
- Campaigns, Postbox, and Reporting
|
- Campaigns, Postbox, and Reporting
|
||||||
- DMS/archive providers
|
- target-tested DMS/archive providers
|
||||||
- policy
|
- policy
|
||||||
- audit
|
|
||||||
- transparency
|
- transparency
|
||||||
|
|
||||||
## Current Boundary
|
## Current Boundary
|
||||||
|
|
||||||
The current kernel supports planned and open records. Restricted per-record
|
The current kernel supports planned/open records, exact filing, closure,
|
||||||
grants, closure, retention calculation, holds, appraisal, disposition,
|
retention calculation, holds, appraisal, independent disposition approval,
|
||||||
transfer, destruction, and external archive effects remain explicit later
|
archive-neutral packaging, recovery evidence, and a clearly marked transfer
|
||||||
work packages. Enabling Records does not trigger destructive or external
|
simulation. The simulation never claims archival custody. Restricted
|
||||||
effects.
|
per-record grants, real target conformance, destructive execution, and real
|
||||||
|
archive effects remain explicit later work packages.
|
||||||
|
|
||||||
|
## Reference Journey
|
||||||
|
|
||||||
|
The executable resident-parking-permit fixture proves equivalent assisted and
|
||||||
|
authenticated digital eAkten. It files the exact form, attachment, case and
|
||||||
|
representation, decision, delivery, and correction revisions; preserves
|
||||||
|
authority, purpose, policy, chronology, and custody evidence; exercises hold
|
||||||
|
and independent approval; and verifies search, temporal reconstruction, and
|
||||||
|
source/package integrity after a SQLite backup/restore round trip. The Records
|
||||||
|
workspace keeps the selected `recordId` in the URL so an authorized actor can
|
||||||
|
resume the same record and inspect the complete evidence chain.
|
||||||
|
|
||||||
The detailed target and implementation sequence are documented in
|
The detailed target and implementation sequence are documented in
|
||||||
[docs/EAKTE_ARCHITECTURE.md](docs/EAKTE_ARCHITECTURE.md).
|
[docs/EAKTE_ARCHITECTURE.md](docs/EAKTE_ARCHITECTURE.md).
|
||||||
|
|||||||
@@ -40,10 +40,14 @@ packages tracked by Records #2-#6:
|
|||||||
- tenant administration for versioned file-plan nodes and record classes,
|
- tenant administration for versioned file-plan nodes and record classes,
|
||||||
explicit volume management, and lifecycle controls in the eAkte workspace.
|
explicit volume management, and lifecycle controls in the eAkte workspace.
|
||||||
|
|
||||||
The remaining boundary is intentionally visible rather than implied: Records
|
The cross-module reference journey from Records #8 is executable: equivalent
|
||||||
#7 requires selection and target testing of an archive/xdomea endpoint and
|
assisted and authenticated digital permit records preserve exact form,
|
||||||
conformance profile; #8 completes the cross-module reference journey and
|
attachment, case/representation, decision, delivery, and correction evidence;
|
||||||
production evidence. Restricted per-record access grants also remain a
|
exercise closure, retention, hold, approval, and simulated transfer; and prove
|
||||||
|
search and temporal reconstruction after database backup/restore. The
|
||||||
|
remaining boundary is intentionally visible rather than implied: Records #7
|
||||||
|
requires selection and target testing of an archive/xdomea endpoint and
|
||||||
|
conformance profile. Restricted per-record access grants also remain a
|
||||||
dedicated access-policy slice. Destruction is represented only as an approved
|
dedicated access-policy slice. Destruction is represented only as an approved
|
||||||
pending state; no content deletion or real archive effect is currently
|
pending state; no content deletion or real archive effect is currently
|
||||||
claimed.
|
claimed.
|
||||||
@@ -234,6 +238,10 @@ available in an evidence/details view.
|
|||||||
5. Add destruction/recovery, TR-ESOR provider integration, migration, and
|
5. Add destruction/recovery, TR-ESOR provider integration, migration, and
|
||||||
signed reference-journey evidence.
|
signed reference-journey evidence.
|
||||||
|
|
||||||
The first reference package should file the digital and assisted variants of
|
The first reference package files the digital and assisted variants of the
|
||||||
the same service-to-decision journey into equivalent records and prove search,
|
resident-parking-permit service-to-decision journey into equivalent records.
|
||||||
historical reconstruction, hold, transfer, restore, and access explanation.
|
Its fixture and regression test prove search, historical reconstruction, hold,
|
||||||
|
independent approval, simulated transfer, restore, current access explanation,
|
||||||
|
source digest revalidation, and transfer-manifest integrity. Its UI contract
|
||||||
|
pins URL-based resume, exact-source filing presets, governance context,
|
||||||
|
evidence semantics, lifecycle actions, and recovery inspection.
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ The native kernel currently provides:
|
|||||||
- independent valid and recorded time with current/at/all temporal reads;
|
- independent valid and recorded time with current/at/all temporal reads;
|
||||||
- tenant, purpose, actor/capacity, source authority, institutional context,
|
- tenant, purpose, actor/capacity, source authority, institutional context,
|
||||||
provenance, and idempotency fields;
|
provenance, and idempotency fields;
|
||||||
- provider-neutral exact-source capabilities, initially implemented by Files
|
- provider-neutral exact-source capabilities implemented by Files versions,
|
||||||
file versions and Cases revisions;
|
Cases revisions, Forms Runtime submissions, and Decisions revisions;
|
||||||
- tenant APIs, search projection, uninstall/retirement guards, and a Records
|
- tenant APIs, search projection, uninstall/retirement guards, and a Records
|
||||||
workspace using shared WebUI controls.
|
workspace using shared WebUI controls.
|
||||||
- immutable close/reopen, retention, hold, appraisal, and independently
|
- immutable close/reopen, retention, hold, appraisal, and independently
|
||||||
@@ -49,6 +49,9 @@ The native kernel currently provides:
|
|||||||
- Core recovery-ledger adoption, atomic terminal evidence, Audit projection,
|
- Core recovery-ledger adoption, atomic terminal evidence, Audit projection,
|
||||||
source and package restore diagnostics, catalog administration, volumes, and
|
source and package restore diagnostics, catalog administration, volumes, and
|
||||||
lifecycle UI.
|
lifecycle UI.
|
||||||
|
- an executable assisted/digital resident-parking-permit reference package
|
||||||
|
that proves equivalent filing semantics, URL resume, search, temporal
|
||||||
|
reconstruction, and evidence integrity after database backup/restore.
|
||||||
|
|
||||||
Restricted object grants remain a separate governed slice. A target-tested
|
Restricted object grants remain a separate governed slice. A target-tested
|
||||||
archive adapter and any destructive effect remain deliberately unimplemented;
|
archive adapter and any destructive effect remain deliberately unimplemented;
|
||||||
@@ -73,12 +76,13 @@ immutable retention evidence; the current record fact receives a
|
|||||||
non-executable manual-review action. Actual correction, closure, appraisal,
|
non-executable manual-review action. Actual correction, closure, appraisal,
|
||||||
hold, disposition, or transfer must use the governed eAkte lifecycle.
|
hold, disposition, or transfer must use the governed eAkte lifecycle.
|
||||||
|
|
||||||
## First Implementation Slice
|
## Next Boundary Slice
|
||||||
|
|
||||||
Complete restricted access and one target-tested archive provider without
|
Complete restricted access and one target-tested archive provider without
|
||||||
moving source-module ownership into Records. Extend the executable reference
|
moving source-module ownership into Records. The executable reference journey
|
||||||
journey across assisted service, decision, filing, hold, restore, search, and
|
already covers assisted/digital intake, decision, filing, hold, restore,
|
||||||
archive simulation before claiming production maturity.
|
search, and archive simulation; it is evidence for the native boundary, not a
|
||||||
|
claim of target archive conformance or transferred custody.
|
||||||
|
|
||||||
The complete native/external boundary, temporal and purpose-aware record model,
|
The complete native/external boundary, temporal and purpose-aware record model,
|
||||||
disposition lifecycle, German public-sector provider profiles, and staged
|
disposition lifecycle, German public-sector provider profiles, and staged
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/records",
|
"name": "@govoplan/records",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GovOPlaN eAkte and institutional records module.",
|
"description": "GovOPlaN eAkte and institutional records module.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-records"
|
name = "govoplan-records"
|
||||||
version = "0.1.19"
|
version = "0.1.20"
|
||||||
description = "GovOPlaN eAkte and institutional records module."
|
description = "GovOPlaN eAkte and institutional records module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ from govoplan_records.backend.archive import (
|
|||||||
|
|
||||||
MODULE_ID = "records"
|
MODULE_ID = "records"
|
||||||
MODULE_NAME = "Records"
|
MODULE_NAME = "Records"
|
||||||
MODULE_VERSION = "0.1.19"
|
MODULE_VERSION = "0.1.20"
|
||||||
READ_SCOPE = "records:workspace:read"
|
READ_SCOPE = "records:workspace:read"
|
||||||
WRITE_SCOPE = "records:workspace:write"
|
WRITE_SCOPE = "records:workspace:write"
|
||||||
ADMIN_SCOPE = "records:workspace:admin"
|
ADMIN_SCOPE = "records:workspace:admin"
|
||||||
@@ -237,7 +237,8 @@ DOCUMENTATION = (
|
|||||||
"volumes, filing decisions, and chronology. Files and other source modules continue to own "
|
"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 "
|
"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 "
|
"module confirms current access. The titlebar temporal selection changes valid and recorded "
|
||||||
"time while current authorization always remains in force."
|
"time while current authorization always remains in force. The selected record is preserved "
|
||||||
|
"as a recordId URL parameter so an authorized actor can resume the same evidence view."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
@@ -288,7 +289,7 @@ DOCUMENTATION = (
|
|||||||
DocumentationTopic(
|
DocumentationTopic(
|
||||||
id="records.filing",
|
id="records.filing",
|
||||||
title="Exact record filing",
|
title="Exact record filing",
|
||||||
summary="File immutable Files or Cases revisions through a provider-neutral capability.",
|
summary="File immutable Files, Cases, Forms Runtime, or Decisions revisions through a provider-neutral capability.",
|
||||||
body=(
|
body=(
|
||||||
"Every filing requires a record, purpose, filing reason, idempotency key, and exact source "
|
"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 "
|
"revision. Records stores source identity, authority mode, digest and content metadata where "
|
||||||
@@ -299,7 +300,14 @@ DOCUMENTATION = (
|
|||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("user", "records_manager", "operator", "module_admin", "auditor"),
|
audience=("user", "records_manager", "operator", "module_admin", "auditor"),
|
||||||
order=110,
|
order=110,
|
||||||
related_modules=("files", "cases", "policy", "audit"),
|
related_modules=(
|
||||||
|
"files",
|
||||||
|
"cases",
|
||||||
|
"forms_runtime",
|
||||||
|
"decisions",
|
||||||
|
"policy",
|
||||||
|
"audit",
|
||||||
|
),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(
|
DocumentationLink(
|
||||||
label="Records domain boundary",
|
label="Records domain boundary",
|
||||||
@@ -389,6 +397,66 @@ DOCUMENTATION = (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="records.reference-journey",
|
||||||
|
title="Assisted and digital eAkte reference journey",
|
||||||
|
summary="Reconstruct the resident-parking-permit evidence chain and verify recovery without implying archive custody.",
|
||||||
|
body=(
|
||||||
|
"Equivalent assisted and authenticated digital resident-parking-permit records preserve exact "
|
||||||
|
"form, attachment, case and representation, formal decision, delivery receipt, and correction "
|
||||||
|
"revisions. The record view explains each source version, authority mode, relationship, filing "
|
||||||
|
"purpose, institutional context, legal basis, retention policy, chronology, hold, independent "
|
||||||
|
"approval, and transfer result. The selected recordId remains in the URL for authorized resume. "
|
||||||
|
"Automated evidence restores a database backup, searches and temporally reconstructs both records, "
|
||||||
|
"and revalidates source digests and the archive-neutral manifest. A simulated receipt explicitly "
|
||||||
|
"states that custody was not transferred; real archive conformance remains target-specific."
|
||||||
|
),
|
||||||
|
layer="configured",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("user", "records_manager", "operator", "module_admin", "auditor"),
|
||||||
|
order=130,
|
||||||
|
related_modules=(
|
||||||
|
"forms_runtime",
|
||||||
|
"files",
|
||||||
|
"cases",
|
||||||
|
"decisions",
|
||||||
|
"approvals",
|
||||||
|
"search",
|
||||||
|
),
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="eAkte architecture",
|
||||||
|
href="govoplan-records/docs/EAKTE_ARCHITECTURE.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
translations={
|
||||||
|
"de": {
|
||||||
|
"title": "Referenzablauf für assistierte und digitale eAkten",
|
||||||
|
"summary": "Die Nachweiskette des Anwohnerparkausweises rekonstruieren und die Wiederherstellung prüfen, ohne Archivverwahrung zu behaupten.",
|
||||||
|
"body": (
|
||||||
|
"Gleichwertige assistierte und authentifizierte digitale Akten zum Anwohnerparkausweis "
|
||||||
|
"bewahren exakte Revisionen von Formular, Anlage, Vorgang und Vertretung, förmlicher "
|
||||||
|
"Entscheidung, Zustellbeleg und Korrektur. Die Aktenansicht erläutert Quellversion, "
|
||||||
|
"Autoritätsmodus, Beziehung, Veraktungszweck, institutionellen Kontext, Rechtsgrundlage, "
|
||||||
|
"Aufbewahrungsrichtlinie, Chronologie, Sperre, unabhängige Freigabe und Übergabeergebnis. "
|
||||||
|
"Die ausgewählte recordId bleibt zum berechtigten Fortsetzen in der URL. Automatisierte "
|
||||||
|
"Nachweise stellen eine Datenbanksicherung wieder her, suchen und rekonstruieren beide "
|
||||||
|
"Akten temporal und prüfen Quellprüfsummen sowie das archivneutrale Manifest erneut. Ein "
|
||||||
|
"simulierter Beleg stellt ausdrücklich klar, dass keine Verwahrung übertragen wurde."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"records.governance-context",
|
||||||
|
"records.record-items",
|
||||||
|
"records.chronology",
|
||||||
|
"records.lifecycle",
|
||||||
|
"records.lifecycle.recovery",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+178
-3
@@ -1,18 +1,193 @@
|
|||||||
{
|
{
|
||||||
"id": "resident-parking-permit-assisted-record",
|
"id": "resident-parking-permit-assisted-record",
|
||||||
"description": "A completed assisted resident parking permit service is filed, closed, held, appraised, independently approved, and exercised through the archive simulation boundary.",
|
"description": "A completed assisted resident parking permit service is filed as an eAkte, closed, held, appraised, independently approved, restored from backup, and exercised through the archive simulation boundary.",
|
||||||
"record": {
|
"record": {
|
||||||
"record_id": "record-1",
|
"record_id": "record-1",
|
||||||
"record_number": "2026/0001",
|
"record_number": "2026/0001",
|
||||||
"class_id": "class-permit"
|
"class_id": "class-permit",
|
||||||
|
"title": "Permit application Ada Example"
|
||||||
},
|
},
|
||||||
|
"institutional_context": {
|
||||||
|
"service_id": "resident-parking-permit",
|
||||||
|
"case_id": "case-parking-2026-0001",
|
||||||
|
"applicant_party_id": "party-ada-example",
|
||||||
|
"representative_party_id": "party-alex-example",
|
||||||
|
"representation_id": "representation-2026-0001",
|
||||||
|
"authority_id": "authority-mobility-berlin",
|
||||||
|
"responsible_function_id": "function-parking-permits",
|
||||||
|
"mandate_id": "mandate-parking-permits-2026",
|
||||||
|
"purpose": "decide the resident parking permit application",
|
||||||
|
"legal_basis": "StVG section 6 and StVO section 45",
|
||||||
|
"retention_policy_ref": "records-policy:parking-permit:v1"
|
||||||
|
},
|
||||||
|
"digital_equivalent": {
|
||||||
|
"record": {
|
||||||
|
"record_id": "record-digital",
|
||||||
|
"record_number": "2026/0002",
|
||||||
|
"class_id": "class-permit",
|
||||||
|
"title": "Permit application Dana Digital"
|
||||||
|
},
|
||||||
|
"institutional_context_overrides": {
|
||||||
|
"case_id": "case-parking-2026-0002",
|
||||||
|
"applicant_party_id": "party-dana-digital",
|
||||||
|
"representative_party_id": "party-dana-digital",
|
||||||
|
"representation_id": "self-representation-2026-0002"
|
||||||
|
},
|
||||||
|
"intake_source": {
|
||||||
|
"id": "digital-submission",
|
||||||
|
"source_module": "forms_runtime",
|
||||||
|
"resource_type": "form_submission_revision",
|
||||||
|
"resource_id": "submission-parking-2026-0002",
|
||||||
|
"source_revision": "revision-2-submitted",
|
||||||
|
"label": "Digital resident parking permit submission",
|
||||||
|
"authority_mode": "native_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 3968,
|
||||||
|
"relationship": "initiates",
|
||||||
|
"purpose": "document the submitted application",
|
||||||
|
"filing_reason": "The authenticated digital submission initiated the administrative procedure.",
|
||||||
|
"launch_url": "/forms-runtime/submissions/submission-parking-2026-0002",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "application",
|
||||||
|
"channel": "digital",
|
||||||
|
"authenticated": true,
|
||||||
|
"form_definition_revision": "resident-parking-permit:v4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"id": "assisted-submission",
|
||||||
|
"source_module": "forms_runtime",
|
||||||
|
"resource_type": "form_submission_revision",
|
||||||
|
"resource_id": "submission-parking-2026-0001",
|
||||||
|
"source_revision": "revision-3-submitted",
|
||||||
|
"label": "Assisted resident parking permit submission",
|
||||||
|
"authority_mode": "native_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 4096,
|
||||||
|
"relationship": "initiates",
|
||||||
|
"purpose": "document the submitted application",
|
||||||
|
"filing_reason": "The assisted, read-back submission initiated the administrative procedure.",
|
||||||
|
"launch_url": "/forms-runtime/submissions/submission-parking-2026-0001",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "application",
|
||||||
|
"channel": "assisted",
|
||||||
|
"read_back_confirmed": true,
|
||||||
|
"form_definition_revision": "resident-parking-permit:v4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "application-attachment",
|
||||||
|
"source_module": "files",
|
||||||
|
"resource_type": "file_version",
|
||||||
|
"resource_id": "file-registration-certificate",
|
||||||
|
"source_revision": "version-2",
|
||||||
|
"label": "Vehicle registration certificate.pdf",
|
||||||
|
"authority_mode": "external_authoritative",
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"size_bytes": 245760,
|
||||||
|
"relationship": "supports",
|
||||||
|
"purpose": "document application eligibility",
|
||||||
|
"filing_reason": "The submitted certificate supports the applicant's vehicle eligibility.",
|
||||||
|
"launch_url": "/files?fileId=file-registration-certificate",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "application_attachment",
|
||||||
|
"malware_scan": "clean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "case-and-representation",
|
||||||
|
"source_module": "cases",
|
||||||
|
"resource_type": "case_revision",
|
||||||
|
"resource_id": "case-parking-2026-0001",
|
||||||
|
"source_revision": "revision-5-representation-verified",
|
||||||
|
"label": "Resident parking permit case with verified representation",
|
||||||
|
"authority_mode": "native_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 8192,
|
||||||
|
"relationship": "governs",
|
||||||
|
"purpose": "document case ownership and representation",
|
||||||
|
"filing_reason": "The case revision identifies the applicant, representative, authority, and verified representation used for the decision.",
|
||||||
|
"launch_url": "/cases/case-parking-2026-0001",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "case_context",
|
||||||
|
"representation_id": "representation-2026-0001",
|
||||||
|
"representation_status": "verified"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "formal-decision",
|
||||||
|
"source_module": "decisions",
|
||||||
|
"resource_type": "decision_revision",
|
||||||
|
"resource_id": "decision-parking-2026-0001",
|
||||||
|
"source_revision": "revision-2-issued",
|
||||||
|
"label": "Resident parking permit decision",
|
||||||
|
"authority_mode": "native_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 6144,
|
||||||
|
"relationship": "decides",
|
||||||
|
"purpose": "document the formal administrative outcome",
|
||||||
|
"filing_reason": "The issued decision records the competent authority, legal basis, outcome, and reasons.",
|
||||||
|
"launch_url": "/decisions/decision-parking-2026-0001",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "formal_decision",
|
||||||
|
"outcome": "approved",
|
||||||
|
"legal_basis": "StVG section 6 and StVO section 45"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "delivery-receipt",
|
||||||
|
"source_module": "files",
|
||||||
|
"resource_type": "file_version",
|
||||||
|
"resource_id": "file-postbox-delivery-receipt",
|
||||||
|
"source_revision": "version-1",
|
||||||
|
"label": "Trusted postbox delivery receipt.json",
|
||||||
|
"authority_mode": "external_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 2048,
|
||||||
|
"relationship": "proves_delivery",
|
||||||
|
"purpose": "document delivery of the formal decision",
|
||||||
|
"filing_reason": "The immutable receipt proves delivery of the issued decision to the represented applicant's trusted postbox.",
|
||||||
|
"launch_url": "/files?fileId=file-postbox-delivery-receipt",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "delivery_receipt",
|
||||||
|
"decision_revision": "revision-2-issued",
|
||||||
|
"delivery_channel": "trusted_postbox",
|
||||||
|
"delivery_status": "delivered"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "correction-evidence",
|
||||||
|
"source_module": "files",
|
||||||
|
"resource_type": "file_version",
|
||||||
|
"resource_id": "file-decision-correction-evidence",
|
||||||
|
"source_revision": "version-1",
|
||||||
|
"label": "Decision address correction evidence.json",
|
||||||
|
"authority_mode": "external_authoritative",
|
||||||
|
"content_type": "application/json",
|
||||||
|
"size_bytes": 2304,
|
||||||
|
"relationship": "corrects",
|
||||||
|
"purpose": "preserve correction chronology without replacing prior evidence",
|
||||||
|
"filing_reason": "The correction evidence links the corrected delivery address to the original decision and receipt while preserving both.",
|
||||||
|
"launch_url": "/files?fileId=file-decision-correction-evidence",
|
||||||
|
"metadata": {
|
||||||
|
"evidence_role": "correction",
|
||||||
|
"corrects_source_id": "delivery-receipt",
|
||||||
|
"previous_evidence_retained": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"expected": {
|
"expected": {
|
||||||
|
"equivalent_record_count": 2,
|
||||||
|
"filed_item_count": 6,
|
||||||
"closed_state": "retention_running",
|
"closed_state": "retention_running",
|
||||||
"appraised_state": "appraised",
|
"appraised_state": "appraised",
|
||||||
"approved_state": "transfer_pending",
|
"approved_state": "transfer_pending",
|
||||||
"hold_blocks_disposition": true,
|
"hold_blocks_disposition": true,
|
||||||
"disposition_status": "approved",
|
"disposition_status": "approved",
|
||||||
"transfer_status": "simulated_accepted",
|
"transfer_status": "simulated_accepted",
|
||||||
"custody_transferred": false
|
"custody_transferred": false,
|
||||||
|
"restored_search_query": "Ada Example"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+503
-1
@@ -2,8 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -73,6 +76,48 @@ class SourceProvider:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReferenceJourneySourceProvider:
|
||||||
|
def __init__(self, provider_id: str, resource_types: tuple[str, ...]) -> None:
|
||||||
|
self.provider_id = provider_id
|
||||||
|
self._resource_types = resource_types
|
||||||
|
|
||||||
|
def resource_types(self):
|
||||||
|
return self._resource_types
|
||||||
|
|
||||||
|
def resolve(self, session, principal, *, locator, purpose):
|
||||||
|
del session, purpose
|
||||||
|
if principal.tenant_id != locator.tenant_id:
|
||||||
|
raise ValueError("Source access denied.")
|
||||||
|
if locator.source_module != self.provider_id:
|
||||||
|
raise ValueError("The source module does not match this provider.")
|
||||||
|
if locator.resource_type not in self._resource_types:
|
||||||
|
raise ValueError("The source type is not supported by this provider.")
|
||||||
|
metadata = dict(locator.metadata)
|
||||||
|
reference = dict(metadata.get("reference") or {})
|
||||||
|
digest_input = ":".join(
|
||||||
|
(
|
||||||
|
locator.source_module,
|
||||||
|
locator.resource_type,
|
||||||
|
locator.resource_id,
|
||||||
|
locator.source_revision,
|
||||||
|
)
|
||||||
|
).encode("utf-8")
|
||||||
|
return RecordSourceReference(
|
||||||
|
locator=locator,
|
||||||
|
label=str(reference.get("label") or locator.resource_id),
|
||||||
|
authority_mode=str(
|
||||||
|
reference.get("authority_mode") or "linked_reference"
|
||||||
|
),
|
||||||
|
content_sha256=hashlib.sha256(digest_input).hexdigest(),
|
||||||
|
content_type=str(reference.get("content_type") or "application/json"),
|
||||||
|
size_bytes=int(reference.get("size_bytes") or 0),
|
||||||
|
valid_from=NOW,
|
||||||
|
recorded_at=NOW,
|
||||||
|
launch_url=str(reference.get("launch_url") or "") or None,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UnknownOutcomeArchiveProvider:
|
class UnknownOutcomeArchiveProvider:
|
||||||
provider_id = "unknown_simulation"
|
provider_id = "unknown_simulation"
|
||||||
|
|
||||||
@@ -131,6 +176,33 @@ class Registry:
|
|||||||
}.get(name)
|
}.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
class ReferenceJourneyRegistry(Registry):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.source_providers_by_module = {
|
||||||
|
"files": ReferenceJourneySourceProvider("files", ("file_version",)),
|
||||||
|
"forms_runtime": ReferenceJourneySourceProvider(
|
||||||
|
"forms_runtime", ("form_submission_revision",)
|
||||||
|
),
|
||||||
|
"cases": ReferenceJourneySourceProvider("cases", ("case_revision",)),
|
||||||
|
"decisions": ReferenceJourneySourceProvider(
|
||||||
|
"decisions", ("decision_revision",)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def capability_names(self):
|
||||||
|
return (
|
||||||
|
*(f"records.source.{name}" for name in self.source_providers_by_module),
|
||||||
|
"records.archive.simulation",
|
||||||
|
"records.archive.unknown_simulation",
|
||||||
|
)
|
||||||
|
|
||||||
|
def tenant_capability(self, name, session, *, tenant_id):
|
||||||
|
if name.startswith("records.source.") and tenant_id == "tenant-1":
|
||||||
|
return self.source_providers_by_module.get(name.removeprefix("records.source."))
|
||||||
|
return super().tenant_capability(name, session, tenant_id=tenant_id)
|
||||||
|
|
||||||
|
|
||||||
class ApprovalProvider:
|
class ApprovalProvider:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.approved = False
|
self.approved = False
|
||||||
@@ -231,7 +303,12 @@ class RecordsTests(unittest.TestCase):
|
|||||||
"file_plan_node_id": "plan-permits",
|
"file_plan_node_id": "plan-permits",
|
||||||
"key": "permit.application",
|
"key": "permit.application",
|
||||||
"label": "Permit application",
|
"label": "Permit application",
|
||||||
"allowed_source_types": ["files:file_version"],
|
"allowed_source_types": [
|
||||||
|
"files:file_version",
|
||||||
|
"forms_runtime:form_submission_revision",
|
||||||
|
"cases:case_revision",
|
||||||
|
"decisions:decision_revision",
|
||||||
|
],
|
||||||
"retention_period_days": 3650,
|
"retention_period_days": 3650,
|
||||||
"access_mode": "tenant",
|
"access_mode": "tenant",
|
||||||
"recorded_at": NOW,
|
"recorded_at": NOW,
|
||||||
@@ -727,6 +804,431 @@ class RecordsTests(unittest.TestCase):
|
|||||||
self.assertTrue(recovery["healthy"])
|
self.assertTrue(recovery["healthy"])
|
||||||
self.assertTrue(recovery["package_checks"][0]["manifest_verified"])
|
self.assertTrue(recovery["package_checks"][0]["manifest_verified"])
|
||||||
|
|
||||||
|
def test_assisted_service_reference_journey_survives_backup_and_restore(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
fixture = json.loads(
|
||||||
|
(
|
||||||
|
Path(__file__).parent / "fixtures/service_to_decision_journey.json"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
expected = fixture["expected"]
|
||||||
|
context = fixture["institutional_context"]
|
||||||
|
self.records = SqlRecordRegistry(ReferenceJourneyRegistry())
|
||||||
|
|
||||||
|
def create_journey_record(
|
||||||
|
record_data: dict[str, object],
|
||||||
|
record_context: dict[str, object],
|
||||||
|
*,
|
||||||
|
idempotency_suffix: str,
|
||||||
|
channel: str,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return self.records.create_record(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
payload={
|
||||||
|
**record_data,
|
||||||
|
"file_plan_node_id": "plan-permits",
|
||||||
|
"description": fixture["description"],
|
||||||
|
"state": "open",
|
||||||
|
"source_authority_mode": "native_authoritative",
|
||||||
|
"access_mode": "tenant",
|
||||||
|
"purpose": record_context["purpose"],
|
||||||
|
"responsible_unit_id": "unit-mobility-services",
|
||||||
|
"responsible_function_id": record_context[
|
||||||
|
"responsible_function_id"
|
||||||
|
],
|
||||||
|
"institutional_context": {**record_context, "channel": channel},
|
||||||
|
"recorded_at": NOW + timedelta(minutes=1),
|
||||||
|
"valid_from": NOW,
|
||||||
|
"change_reason": f"The {channel} application was accepted for processing.",
|
||||||
|
"idempotency_key": f"reference-record-create-{idempotency_suffix}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def filing_request(
|
||||||
|
record_id: str,
|
||||||
|
source: dict[str, object],
|
||||||
|
record_context: dict[str, object],
|
||||||
|
*,
|
||||||
|
idempotency_suffix: str = "",
|
||||||
|
) -> RecordFilingRequest:
|
||||||
|
metadata = dict(source["metadata"])
|
||||||
|
return RecordFilingRequest(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
record_id=record_id,
|
||||||
|
source=RecordSourceLocator(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
source_module=str(source["source_module"]),
|
||||||
|
resource_type=str(source["resource_type"]),
|
||||||
|
resource_id=str(source["resource_id"]),
|
||||||
|
source_revision=str(source["source_revision"]),
|
||||||
|
metadata={
|
||||||
|
"reference": {
|
||||||
|
"label": source["label"],
|
||||||
|
"authority_mode": source["authority_mode"],
|
||||||
|
"content_type": source["content_type"],
|
||||||
|
"size_bytes": source["size_bytes"],
|
||||||
|
"launch_url": source["launch_url"],
|
||||||
|
},
|
||||||
|
**metadata,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
purpose=str(source["purpose"]),
|
||||||
|
filing_reason=str(source["filing_reason"]),
|
||||||
|
relationship=str(source["relationship"]),
|
||||||
|
institutional_context={
|
||||||
|
**record_context,
|
||||||
|
"evidence_role": metadata["evidence_role"],
|
||||||
|
},
|
||||||
|
metadata={"fixture_source_id": source["id"]},
|
||||||
|
idempotency_key=(
|
||||||
|
f"reference-file-{source['id']}{idempotency_suffix}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
record = create_journey_record(
|
||||||
|
fixture["record"], context, idempotency_suffix="assisted", channel="assisted"
|
||||||
|
)
|
||||||
|
self.assertEqual(fixture["record"]["title"], record["title"])
|
||||||
|
|
||||||
|
filed_results = []
|
||||||
|
for source in fixture["sources"]:
|
||||||
|
filed_results.append(
|
||||||
|
self.records.file(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
request=filing_request(str(record["record_id"]), source, context),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
replay = self.records.file(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
request=filing_request(
|
||||||
|
str(record["record_id"]), fixture["sources"][0], context
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertTrue(replay.replayed)
|
||||||
|
self.assertEqual(filed_results[0].item_id, replay.item_id)
|
||||||
|
|
||||||
|
detail = self.records.get_record(
|
||||||
|
self.session, self.principal, record_id=str(record["record_id"])
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["filed_item_count"], len(detail["items"]))
|
||||||
|
self.assertEqual(
|
||||||
|
[source["source_revision"] for source in fixture["sources"]],
|
||||||
|
[item["source"]["source_revision"] for item in detail["items"]],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[source["relationship"] for source in fixture["sources"]],
|
||||||
|
[item["relationship"] for item in detail["items"]],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"application",
|
||||||
|
"application_attachment",
|
||||||
|
"case_context",
|
||||||
|
"formal_decision",
|
||||||
|
"delivery_receipt",
|
||||||
|
"correction",
|
||||||
|
},
|
||||||
|
{item["source_metadata"]["evidence_role"] for item in detail["items"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
digital_fixture = fixture["digital_equivalent"]
|
||||||
|
digital_context = {
|
||||||
|
**context,
|
||||||
|
**digital_fixture["institutional_context_overrides"],
|
||||||
|
}
|
||||||
|
digital_record = create_journey_record(
|
||||||
|
digital_fixture["record"],
|
||||||
|
digital_context,
|
||||||
|
idempotency_suffix="digital",
|
||||||
|
channel="digital",
|
||||||
|
)
|
||||||
|
digital_sources = [digital_fixture["intake_source"]]
|
||||||
|
for source in fixture["sources"][1:]:
|
||||||
|
digital_sources.append(
|
||||||
|
{
|
||||||
|
**source,
|
||||||
|
"id": f"digital-{source['id']}",
|
||||||
|
"resource_id": f"{source['resource_id']}-digital",
|
||||||
|
"label": f"Digital equivalent · {source['label']}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for source in digital_sources:
|
||||||
|
self.records.file(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
request=filing_request(
|
||||||
|
str(digital_record["record_id"]),
|
||||||
|
source,
|
||||||
|
digital_context,
|
||||||
|
idempotency_suffix="-digital",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
digital_detail = self.records.get_record(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(digital_record["record_id"]),
|
||||||
|
)
|
||||||
|
def equivalence_fields(item):
|
||||||
|
return (
|
||||||
|
item["source"]["source_module"],
|
||||||
|
item["source"]["resource_type"],
|
||||||
|
item["relationship"],
|
||||||
|
item["source_metadata"]["evidence_role"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[equivalence_fields(item) for item in detail["items"]],
|
||||||
|
[equivalence_fields(item) for item in digital_detail["items"]],
|
||||||
|
)
|
||||||
|
all_records, total_records = self.records.list_records(
|
||||||
|
self.session, self.principal
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["equivalent_record_count"], total_records)
|
||||||
|
self.assertEqual(
|
||||||
|
{fixture["record"]["record_id"], digital_fixture["record"]["record_id"]},
|
||||||
|
{item["record_id"] for item in all_records},
|
||||||
|
)
|
||||||
|
|
||||||
|
lifecycle_start = datetime.now(UTC).replace(microsecond=0) + timedelta(
|
||||||
|
minutes=10
|
||||||
|
)
|
||||||
|
closed = self.records.close_record(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_revision": 1,
|
||||||
|
"purpose": "close completed resident parking permit record",
|
||||||
|
"reason": "Decision, delivery, and correction evidence are complete.",
|
||||||
|
"recorded_at": lifecycle_start,
|
||||||
|
"idempotency_key": "reference-close",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["closed_state"], closed["state"])
|
||||||
|
appraised = self.records.appraise_record(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_revision": 2,
|
||||||
|
"outcome": "transfer",
|
||||||
|
"purpose": "appraise completed resident parking permit record",
|
||||||
|
"reason": "Offer the complete record after governed review.",
|
||||||
|
"policy_refs": [context["retention_policy_ref"]],
|
||||||
|
"override_retention_not_due": True,
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=1),
|
||||||
|
"idempotency_key": "reference-appraise",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["appraised_state"], appraised["state"])
|
||||||
|
hold = self.records.apply_hold(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_record_revision": 3,
|
||||||
|
"reason": "Preserve the record while a correction is reviewed.",
|
||||||
|
"authority": "Mobility authority review 2026-17",
|
||||||
|
"purpose": "preserve correction evidence",
|
||||||
|
"policy_refs": [context["retention_policy_ref"]],
|
||||||
|
"institutional_context": context,
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=2),
|
||||||
|
"idempotency_key": "reference-hold",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(RecordConflictError, "active record hold"):
|
||||||
|
self.records.propose_disposition(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_record_revision": 3,
|
||||||
|
"action": "transfer",
|
||||||
|
"reason": "Attempt transfer while evidence is held.",
|
||||||
|
"purpose": "prove hold enforcement",
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=3),
|
||||||
|
"idempotency_key": "reference-disposition-blocked",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.records.release_hold(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
hold_id=str(hold["hold_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_hold_revision": 1,
|
||||||
|
"reason": "Correction review completed with prior evidence preserved.",
|
||||||
|
"purpose": "resume governed disposition",
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=4),
|
||||||
|
"idempotency_key": "reference-hold-release",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
disposition = self.records.propose_disposition(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"disposition_id": "reference-disposition",
|
||||||
|
"expected_record_revision": 3,
|
||||||
|
"action": "transfer",
|
||||||
|
"reason": "The complete record is ready for an independently reviewed offer.",
|
||||||
|
"purpose": "prepare governed archive offer",
|
||||||
|
"policy_refs": [context["retention_policy_ref"]],
|
||||||
|
"institutional_context": context,
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=5),
|
||||||
|
"idempotency_key": "reference-disposition",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.records.registry.approvals.approved = True
|
||||||
|
finalized = self.records.finalize_disposition(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
disposition_id=str(disposition["disposition_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_disposition_revision": 1,
|
||||||
|
"purpose": "record independent disposition approval",
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=6),
|
||||||
|
"idempotency_key": "reference-disposition-finalize",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["approved_state"], finalized["record"]["state"])
|
||||||
|
package = self.records.prepare_transfer_package(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
payload={
|
||||||
|
"package_id": "reference-transfer-package",
|
||||||
|
"disposition_id": str(disposition["disposition_id"]),
|
||||||
|
"expected_record_revision": 4,
|
||||||
|
"provider_id": "simulation",
|
||||||
|
"profile": "govoplan-simulation-v1",
|
||||||
|
"purpose": "validate archive transfer boundary",
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=7),
|
||||||
|
"idempotency_key": "reference-transfer-prepare",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
receipt = self.records.dispatch_transfer_package(
|
||||||
|
self.session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
package_id=str(package["package_id"]),
|
||||||
|
payload={
|
||||||
|
"expected_package_revision": 1,
|
||||||
|
"purpose": "validate archive receipt handling",
|
||||||
|
"recorded_at": lifecycle_start + timedelta(minutes=8),
|
||||||
|
"idempotency_key": "reference-transfer-dispatch",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["transfer_status"], receipt["status"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected["custody_transferred"],
|
||||||
|
receipt["receipt"]["metadata"]["custody_transferred"],
|
||||||
|
)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-records-restore-") as temp_dir:
|
||||||
|
backup_path = Path(temp_dir) / "restored-records.sqlite3"
|
||||||
|
source_connection = self.engine.raw_connection()
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(backup_path) as backup_connection:
|
||||||
|
source_connection.driver_connection.backup(backup_connection)
|
||||||
|
finally:
|
||||||
|
source_connection.close()
|
||||||
|
|
||||||
|
restored_engine = create_engine(f"sqlite+pysqlite:///{backup_path}")
|
||||||
|
restored_session = Session(restored_engine)
|
||||||
|
try:
|
||||||
|
restored_records = SqlRecordRegistry(ReferenceJourneyRegistry())
|
||||||
|
matches, total = restored_records.list_records(
|
||||||
|
restored_session,
|
||||||
|
self.principal,
|
||||||
|
query=expected["restored_search_query"],
|
||||||
|
)
|
||||||
|
self.assertEqual(1, total)
|
||||||
|
self.assertEqual(record["record_id"], matches[0]["record_id"])
|
||||||
|
|
||||||
|
restored = restored_records.get_record(
|
||||||
|
restored_session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
)
|
||||||
|
self.assertEqual(expected["filed_item_count"], len(restored["items"]))
|
||||||
|
self.assertEqual(
|
||||||
|
{**context, "channel": "assisted"},
|
||||||
|
restored["record"]["institutional_context"],
|
||||||
|
)
|
||||||
|
self.assertEqual("released", restored["holds"][0]["status"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected["disposition_status"],
|
||||||
|
restored["dispositions"][0]["status"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[context["retention_policy_ref"]],
|
||||||
|
restored["dispositions"][0]["policy_refs"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
expected["transfer_status"],
|
||||||
|
restored["transfer_packages"][0]["status"],
|
||||||
|
)
|
||||||
|
self.assertTrue(restored["transfer_packages"][0]["simulated"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected["filed_item_count"],
|
||||||
|
sum(
|
||||||
|
entry["event_type"] == "record.item_filed"
|
||||||
|
for entry in restored["chronology"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
recovery = restored_records.recovery_status(
|
||||||
|
restored_session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
)
|
||||||
|
self.assertTrue(recovery["healthy"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected["filed_item_count"], len(recovery["source_checks"])
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
check["status"] == "verified"
|
||||||
|
for check in recovery["source_checks"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
check["manifest_verified"]
|
||||||
|
for check in recovery["package_checks"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
token = bind_temporal_data_context(
|
||||||
|
TemporalDataContext(
|
||||||
|
validity_mode="at",
|
||||||
|
valid_at=lifecycle_start - timedelta(minutes=1),
|
||||||
|
recorded_at=lifecycle_start - timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
historical = restored_records.get_record(
|
||||||
|
restored_session,
|
||||||
|
self.principal,
|
||||||
|
record_id=str(record["record_id"]),
|
||||||
|
)
|
||||||
|
self.assertEqual("open", historical["record"]["state"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected["filed_item_count"], len(historical["items"])
|
||||||
|
)
|
||||||
|
self.assertEqual([], historical["transfer_packages"])
|
||||||
|
finally:
|
||||||
|
reset_temporal_data_context(token)
|
||||||
|
finally:
|
||||||
|
restored_session.close()
|
||||||
|
restored_engine.dispose()
|
||||||
|
|
||||||
def test_disposition_waits_for_optional_approvals_and_can_be_withdrawn(
|
def test_disposition_waits_for_optional_approvals_and_can_be_withdrawn(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class ReferenceJourneyUiTests(unittest.TestCase):
|
||||||
|
def test_actor_can_resume_and_reconstruct_the_record_journey(self) -> None:
|
||||||
|
page = (ROOT / "webui/src/features/records/RecordsPage.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
lifecycle = (
|
||||||
|
ROOT / "webui/src/features/records/RecordLifecyclePanel.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
api = (ROOT / "webui/src/api/records.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for marker in (
|
||||||
|
'searchParams.get("recordId")',
|
||||||
|
'next.set("recordId", selectedRecordId)',
|
||||||
|
"filingPresetFrom(searchParams)",
|
||||||
|
'searchParams.get("sourceRevision")',
|
||||||
|
'<WorkspaceActionBar',
|
||||||
|
"refreshable",
|
||||||
|
'reloadAction={{ onReload: reload',
|
||||||
|
'data-help-context-id="records.governance-context"',
|
||||||
|
'data-help-context-id="records.record-items"',
|
||||||
|
'data-help-context-id="records.chronology"',
|
||||||
|
"item.source.source_revision",
|
||||||
|
"item.authority_mode",
|
||||||
|
"item.relationship",
|
||||||
|
"item.purpose",
|
||||||
|
"item.source_metadata.evidence_role",
|
||||||
|
"detail.access_explanation.current_authorization",
|
||||||
|
):
|
||||||
|
self.assertIn(marker, page)
|
||||||
|
|
||||||
|
for action in (
|
||||||
|
'openAction("close")',
|
||||||
|
'openAction("reopen")',
|
||||||
|
'openAction("appraise")',
|
||||||
|
'openAction("hold")',
|
||||||
|
'openAction("release-hold", holdId)',
|
||||||
|
'openAction("disposition")',
|
||||||
|
'openAction("finalize")',
|
||||||
|
'openAction("prepare-transfer")',
|
||||||
|
'openAction("dispatch-transfer")',
|
||||||
|
"openRecovery",
|
||||||
|
"Release all active holds first.",
|
||||||
|
"Simulation only",
|
||||||
|
"custody",
|
||||||
|
"item.policy_refs",
|
||||||
|
):
|
||||||
|
self.assertIn(action, lifecycle)
|
||||||
|
|
||||||
|
for endpoint in (
|
||||||
|
'"close"',
|
||||||
|
'"reopen"',
|
||||||
|
'"appraise"',
|
||||||
|
'"holds"',
|
||||||
|
'"dispositions"',
|
||||||
|
'"transfer-packages"',
|
||||||
|
"/recovery`",
|
||||||
|
):
|
||||||
|
self.assertIn(endpoint, api)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/records-webui",
|
"name": "@govoplan/records-webui",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -89,9 +89,15 @@ export type RecordItem = {
|
|||||||
content_sha256?: string | null;
|
content_sha256?: string | null;
|
||||||
content_type?: string | null;
|
content_type?: string | null;
|
||||||
size_bytes?: number | null;
|
size_bytes?: number | null;
|
||||||
|
source_valid_from?: string | null;
|
||||||
|
source_valid_to?: string | null;
|
||||||
|
source_recorded_at?: string | null;
|
||||||
launch_url?: string | null;
|
launch_url?: string | null;
|
||||||
filed_at: string;
|
filed_at: string;
|
||||||
filed_by?: string | null;
|
filed_by?: string | null;
|
||||||
|
institutional_context: Record<string, unknown>;
|
||||||
|
source_metadata: Record<string, unknown>;
|
||||||
|
filing_metadata: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RecordChronology = {
|
export type RecordChronology = {
|
||||||
@@ -147,10 +153,15 @@ export type RecordTransferPackage = {
|
|||||||
provider_id: string;
|
provider_id: string;
|
||||||
profile: string;
|
profile: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
authority_mode: string;
|
||||||
|
manifest: Record<string, unknown>;
|
||||||
manifest_sha256: string;
|
manifest_sha256: string;
|
||||||
|
receipt: Record<string, unknown>;
|
||||||
receipt_sha256?: string | null;
|
receipt_sha256?: string | null;
|
||||||
external_reference?: string | null;
|
external_reference?: string | null;
|
||||||
|
recovery_operation_id?: string | null;
|
||||||
simulated: boolean;
|
simulated: boolean;
|
||||||
|
institutional_context: Record<string, unknown>;
|
||||||
recorded_at: string;
|
recorded_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ function LifecycleEvidence({ detail, onRelease, canWrite }: { detail: RecordDeta
|
|||||||
<div><dt>Appraisal</dt><dd>{record.appraisal_state ? humanize(record.appraisal_state) : "Not appraised"}</dd></div>
|
<div><dt>Appraisal</dt><dd>{record.appraisal_state ? humanize(record.appraisal_state) : "Not appraised"}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{detail.volumes.length > 0 && <div className="records-evidence-list"><strong>Volumes</strong>{detail.volumes.map((volume) => <div key={volume.volume_id}><span>{volume.sequence}. {volume.label}</span><StatusBadge status={volume.state === "open" ? "active" : "neutral"} label={humanize(volume.state)} /></div>)}</div>}
|
{detail.volumes.length > 0 && <div className="records-evidence-list"><strong>Volumes</strong>{detail.volumes.map((volume) => <div key={volume.volume_id}><span>{volume.sequence}. {volume.label}</span><StatusBadge status={volume.state === "open" ? "active" : "neutral"} label={humanize(volume.state)} /></div>)}</div>}
|
||||||
{detail.holds.length > 0 && <div className="records-evidence-list"><strong>Holds</strong>{detail.holds.map((hold) => <div key={hold.hold_id}><span><b>{hold.authority}</b> · {hold.reason}</span><span className="records-evidence-actions"><StatusBadge status={hold.status === "active" ? "danger" : "neutral"} label={humanize(hold.status)} />{hold.status === "active" && <Button type="button" variant="ghost" iconOnly aria-label="Release hold" title="Release hold" onClick={() => onRelease(hold.hold_id)} disabledReason={!canWrite ? "Your account may not release holds." : undefined}><Ban size={15} aria-hidden="true" /></Button>}</span></div>)}</div>}
|
{detail.holds.length > 0 && <div className="records-evidence-list"><strong>Holds</strong>{detail.holds.map((hold) => <div key={hold.hold_id}><span><b>{hold.authority}</b> · {hold.reason}{hold.policy_refs.length > 0 ? ` · policy ${hold.policy_refs.join(", ")}` : ""}</span><span className="records-evidence-actions"><StatusBadge status={hold.status === "active" ? "danger" : "neutral"} label={humanize(hold.status)} />{hold.status === "active" && <Button type="button" variant="ghost" iconOnly aria-label="Release hold" title="Release hold" onClick={() => onRelease(hold.hold_id)} disabledReason={!canWrite ? "Your account may not release holds." : undefined}><Ban size={15} aria-hidden="true" /></Button>}</span></div>)}</div>}
|
||||||
{detail.dispositions.map((item) => <DispositionEvidence key={item.disposition_id} item={item} />)}
|
{detail.dispositions.map((item) => <DispositionEvidence key={item.disposition_id} item={item} />)}
|
||||||
{detail.transfer_packages.map((item) => <TransferEvidence key={item.package_id} item={item} />)}
|
{detail.transfer_packages.map((item) => <TransferEvidence key={item.package_id} item={item} />)}
|
||||||
</div>
|
</div>
|
||||||
@@ -322,11 +322,20 @@ function LifecycleEvidence({ detail, onRelease, canWrite }: { detail: RecordDeta
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DispositionEvidence({ item }: { item: RecordDisposition }) {
|
function DispositionEvidence({ item }: { item: RecordDisposition }) {
|
||||||
return <div className="records-evidence-list"><strong>Disposition</strong><div><span><b>{humanize(item.action)}</b> · {item.reason}</span><StatusBadge status={item.status === "approved" ? "active" : item.status === "review_unavailable" ? "danger" : "warning"} label={humanize(item.status)} /></div>{item.approval_request_id && <small>Approval: {item.approval_request_id}</small>}</div>;
|
return <div className="records-evidence-list"><strong>Disposition</strong><div><span><b>{humanize(item.action)}</b> · {item.reason}</span><StatusBadge status={item.status === "approved" ? "active" : item.status === "review_unavailable" ? "danger" : "warning"} label={humanize(item.status)} /></div>{item.policy_refs.length > 0 && <small>Policy: {item.policy_refs.join(", ")}</small>}{item.approval_request_id && <small>Approval: {item.approval_request_id}</small>}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferEvidence({ item }: { item: RecordTransferPackage }) {
|
function TransferEvidence({ item }: { item: RecordTransferPackage }) {
|
||||||
return <div className="records-evidence-list"><strong>Transfer package</strong><div><span><b>{item.profile}</b> · {item.manifest_sha256.slice(0, 12)}</span><StatusBadge status={item.status === "simulated_accepted" ? "warning" : item.status === "accepted" ? "active" : "neutral"} label={humanize(item.status)} /></div>{item.simulated && <small>Simulation only; no archival custody.</small>}</div>;
|
const receiptMetadata = typeof item.receipt.metadata === "object" && item.receipt.metadata != null
|
||||||
|
? item.receipt.metadata as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
const custodyTransferred = receiptMetadata.custody_transferred === true;
|
||||||
|
const custodySummary = item.simulated
|
||||||
|
? "Simulation only; no archival custody."
|
||||||
|
: custodyTransferred
|
||||||
|
? "Archive custody was transferred."
|
||||||
|
: "Archive custody was not transferred.";
|
||||||
|
return <div className="records-evidence-list"><strong>Transfer package</strong><div><span><b>{item.profile}</b> · {item.manifest_sha256.slice(0, 12)}</span><StatusBadge status={item.status === "simulated_accepted" ? "warning" : item.status === "accepted" ? "active" : "neutral"} label={humanize(item.status)} /></div><small>{custodySummary} Authority: {humanize(item.authority_mode)}.</small></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RecoveryResult({ value, loading }: { value: Record<string, unknown> | null; loading: boolean }) {
|
function RecoveryResult({ value, loading }: { value: Record<string, unknown> | null; loading: boolean }) {
|
||||||
|
|||||||
@@ -464,6 +464,7 @@ function RecordDetailPanel({
|
|||||||
onChanged: () => void;
|
onChanged: () => void;
|
||||||
}) {
|
}) {
|
||||||
const record = detail.record;
|
const record = detail.record;
|
||||||
|
const governanceContext = recordContextFacts(record.institutional_context);
|
||||||
return (
|
return (
|
||||||
<PageScrollViewport className="records-detail-scroll">
|
<PageScrollViewport className="records-detail-scroll">
|
||||||
<div className="records-detail-header">
|
<div className="records-detail-header">
|
||||||
@@ -496,6 +497,17 @@ function RecordDetailPanel({
|
|||||||
|
|
||||||
{record.description && <p className="records-description">{record.description}</p>}
|
{record.description && <p className="records-description">{record.description}</p>}
|
||||||
|
|
||||||
|
{governanceContext.length > 0 && (
|
||||||
|
<section className="records-detail-section" data-help-context-id="records.governance-context">
|
||||||
|
<div className="records-section-heading"><h2>Governance context</h2></div>
|
||||||
|
<dl className="records-context-facts">
|
||||||
|
{governanceContext.map(([label, value]) => (
|
||||||
|
<div key={label}><dt>{label}</dt><dd>{value}</dd></div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<RecordLifecyclePanel
|
<RecordLifecyclePanel
|
||||||
detail={detail}
|
detail={detail}
|
||||||
archiveProviders={archiveProviders}
|
archiveProviders={archiveProviders}
|
||||||
@@ -525,6 +537,15 @@ function RecordDetailPanel({
|
|||||||
<time>{formatDateTime(item.filed_at)}</time>
|
<time>{formatDateTime(item.filed_at)}</time>
|
||||||
</div>
|
</div>
|
||||||
<p>{item.filing_reason}</p>
|
<p>{item.filing_reason}</p>
|
||||||
|
<dl className="records-item-evidence">
|
||||||
|
<div><dt>Authority</dt><dd>{humanize(item.authority_mode)}</dd></div>
|
||||||
|
<div><dt>Relationship</dt><dd>{humanize(item.relationship)}</dd></div>
|
||||||
|
<div><dt>Purpose</dt><dd>{item.purpose}</dd></div>
|
||||||
|
<div><dt>Source recorded</dt><dd>{formatDateTime(item.source_recorded_at)}</dd></div>
|
||||||
|
{typeof item.source_metadata.evidence_role === "string" && (
|
||||||
|
<div><dt>Evidence role</dt><dd>{humanize(item.source_metadata.evidence_role)}</dd></div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -882,6 +903,27 @@ function formatBytes(value?: number | null): string {
|
|||||||
return `${(value / 1024 / 1024).toFixed(1)} MiB`;
|
return `${(value / 1024 / 1024).toFixed(1)} MiB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordContextFacts(context: Record<string, unknown>): Array<[string, string]> {
|
||||||
|
const fields: Array<[string, string]> = [
|
||||||
|
["Service", "service_id"],
|
||||||
|
["Case", "case_id"],
|
||||||
|
["Applicant", "applicant_party_id"],
|
||||||
|
["Representative", "representative_party_id"],
|
||||||
|
["Representation", "representation_id"],
|
||||||
|
["Authority", "authority_id"],
|
||||||
|
["Responsible function", "responsible_function_id"],
|
||||||
|
["Mandate", "mandate_id"],
|
||||||
|
["Legal basis", "legal_basis"],
|
||||||
|
["Retention policy", "retention_policy_ref"]
|
||||||
|
];
|
||||||
|
return fields.flatMap(([label, key]) => {
|
||||||
|
const value = context[key];
|
||||||
|
if (typeof value === "string" && value.trim()) return [[label, value]];
|
||||||
|
if (Array.isArray(value) && value.length > 0) return [[label, value.map(String).join(", ")]];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function humanize(value: string): string {
|
function humanize(value: string): string {
|
||||||
return value.replace(/[_:.\-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
return value.replace(/[_:.\-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ const en = {
|
|||||||
"Retention input": "Retention input",
|
"Retention input": "Retention input",
|
||||||
"Not classified": "Not classified",
|
"Not classified": "Not classified",
|
||||||
"Not configured": "Not configured",
|
"Not configured": "Not configured",
|
||||||
|
"Governance context": "Governance context",
|
||||||
|
"Service": "Service",
|
||||||
|
"Case": "Case",
|
||||||
|
"Applicant": "Applicant",
|
||||||
|
"Representative": "Representative",
|
||||||
|
"Representation": "Representation",
|
||||||
|
"Responsible function": "Responsible function",
|
||||||
|
"Mandate": "Mandate",
|
||||||
|
"Legal basis": "Legal basis",
|
||||||
|
"Retention policy": "Retention policy",
|
||||||
"Contents": "Contents",
|
"Contents": "Contents",
|
||||||
"No items have been filed in this temporal view.": "No items have been filed in this temporal view.",
|
"No items have been filed in this temporal view.": "No items have been filed in this temporal view.",
|
||||||
"Chronology": "Chronology",
|
"Chronology": "Chronology",
|
||||||
@@ -66,6 +76,9 @@ const en = {
|
|||||||
"Source object ID": "Source object ID",
|
"Source object ID": "Source object ID",
|
||||||
"Exact source revision": "Exact source revision",
|
"Exact source revision": "Exact source revision",
|
||||||
"Filing reason": "Filing reason",
|
"Filing reason": "Filing reason",
|
||||||
|
"Relationship": "Relationship",
|
||||||
|
"Source recorded": "Source recorded",
|
||||||
|
"Evidence role": "Evidence role",
|
||||||
"Not set": "Not set",
|
"Not set": "Not set",
|
||||||
"Size unavailable": "Size unavailable",
|
"Size unavailable": "Size unavailable",
|
||||||
"Your account may view records but may not create them.": "Your account may view records but may not create them.",
|
"Your account may view records but may not create them.": "Your account may view records but may not create them.",
|
||||||
@@ -121,6 +134,8 @@ const en = {
|
|||||||
"Disposition": "Disposition",
|
"Disposition": "Disposition",
|
||||||
"Transfer package": "Transfer package",
|
"Transfer package": "Transfer package",
|
||||||
"Simulation only; no archival custody.": "Simulation only; no archival custody.",
|
"Simulation only; no archival custody.": "Simulation only; no archival custody.",
|
||||||
|
"Archive custody was transferred.": "Archive custody was transferred.",
|
||||||
|
"Archive custody was not transferred.": "Archive custody was not transferred.",
|
||||||
"Volume label": "Volume label",
|
"Volume label": "Volume label",
|
||||||
"Authority": "Authority",
|
"Authority": "Authority",
|
||||||
"Explicit retention trigger": "Explicit retention trigger",
|
"Explicit retention trigger": "Explicit retention trigger",
|
||||||
@@ -194,6 +209,16 @@ const de: Record<keyof typeof en, string> = {
|
|||||||
"Retention input": "Aufbewahrungsvorgabe",
|
"Retention input": "Aufbewahrungsvorgabe",
|
||||||
"Not classified": "Nicht klassifiziert",
|
"Not classified": "Nicht klassifiziert",
|
||||||
"Not configured": "Nicht konfiguriert",
|
"Not configured": "Nicht konfiguriert",
|
||||||
|
"Governance context": "Governance-Kontext",
|
||||||
|
"Service": "Leistung",
|
||||||
|
"Case": "Vorgang",
|
||||||
|
"Applicant": "Antragstellende Person",
|
||||||
|
"Representative": "Vertretende Person",
|
||||||
|
"Representation": "Vertretung",
|
||||||
|
"Responsible function": "Zuständige Funktion",
|
||||||
|
"Mandate": "Mandat",
|
||||||
|
"Legal basis": "Rechtsgrundlage",
|
||||||
|
"Retention policy": "Aufbewahrungsrichtlinie",
|
||||||
"Contents": "Inhalt",
|
"Contents": "Inhalt",
|
||||||
"No items have been filed in this temporal view.": "In dieser temporalen Ansicht sind keine Objekte veraktet.",
|
"No items have been filed in this temporal view.": "In dieser temporalen Ansicht sind keine Objekte veraktet.",
|
||||||
"Chronology": "Chronologie",
|
"Chronology": "Chronologie",
|
||||||
@@ -219,6 +244,9 @@ const de: Record<keyof typeof en, string> = {
|
|||||||
"Source object ID": "ID des Quellobjekts",
|
"Source object ID": "ID des Quellobjekts",
|
||||||
"Exact source revision": "Exakte Quellrevision",
|
"Exact source revision": "Exakte Quellrevision",
|
||||||
"Filing reason": "Veraktungsbegründung",
|
"Filing reason": "Veraktungsbegründung",
|
||||||
|
"Relationship": "Beziehung",
|
||||||
|
"Source recorded": "Quelle erfasst am",
|
||||||
|
"Evidence role": "Nachweisrolle",
|
||||||
"Not set": "Nicht gesetzt",
|
"Not set": "Nicht gesetzt",
|
||||||
"Size unavailable": "Größe nicht verfügbar",
|
"Size unavailable": "Größe nicht verfügbar",
|
||||||
"Your account may view records but may not create them.": "Ihr Konto darf Akten einsehen, aber nicht anlegen.",
|
"Your account may view records but may not create them.": "Ihr Konto darf Akten einsehen, aber nicht anlegen.",
|
||||||
@@ -274,6 +302,8 @@ const de: Record<keyof typeof en, string> = {
|
|||||||
"Disposition": "Aussonderung",
|
"Disposition": "Aussonderung",
|
||||||
"Transfer package": "Übergabepaket",
|
"Transfer package": "Übergabepaket",
|
||||||
"Simulation only; no archival custody.": "Nur Simulation; keine Archivverwahrung.",
|
"Simulation only; no archival custody.": "Nur Simulation; keine Archivverwahrung.",
|
||||||
|
"Archive custody was transferred.": "Die Archivverwahrung wurde übertragen.",
|
||||||
|
"Archive custody was not transferred.": "Die Archivverwahrung wurde nicht übertragen.",
|
||||||
"Volume label": "Bandbezeichnung",
|
"Volume label": "Bandbezeichnung",
|
||||||
"Authority": "Anordnende Stelle",
|
"Authority": "Anordnende Stelle",
|
||||||
"Explicit retention trigger": "Expliziter Aufbewahrungsbeginn",
|
"Explicit retention trigger": "Expliziter Aufbewahrungsbeginn",
|
||||||
|
|||||||
@@ -253,6 +253,42 @@
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.records-context-facts,
|
||||||
|
.records-item-evidence {
|
||||||
|
display: grid;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-context-facts {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: var(--border-line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-context-facts > div {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 9px 10px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-context-facts dt,
|
||||||
|
.records-item-evidence dt {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-context-facts dd,
|
||||||
|
.records-item-evidence dd {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
margin: 3px 0 0;
|
||||||
|
color: var(--text-strong);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.records-lifecycle-actions {
|
.records-lifecycle-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -424,6 +460,17 @@
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.records-item-evidence {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-item-evidence > div {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.records-chronology > div {
|
.records-chronology > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -564,6 +611,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.records-facts,
|
.records-facts,
|
||||||
|
.records-context-facts,
|
||||||
|
.records-item-evidence,
|
||||||
.records-lifecycle-evidence > dl,
|
.records-lifecycle-evidence > dl,
|
||||||
.records-recovery-result dl,
|
.records-recovery-result dl,
|
||||||
.records-access-explanation dl,
|
.records-access-explanation dl,
|
||||||
|
|||||||
Reference in New Issue
Block a user