feat(dms): add dvelop d3 integration profile
Module Package Release / publish-packages (push) Successful in 10s
Module Package Release / publish-packages (push) Successful in 10s
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""GovOPlaN DMS integration module."""
|
||||
|
||||
__version__ = "0.1.19"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Backend integration profiles for document-management systems."""
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Protocol
|
||||
from urllib.parse import quote, urljoin, urlsplit, urlunsplit
|
||||
|
||||
from govoplan_core.core.records import (
|
||||
RecordArchiveProviderState,
|
||||
RecordArchiveReceipt,
|
||||
RecordArchiveTransferRequest,
|
||||
RecordContractError,
|
||||
)
|
||||
from govoplan_core.security.http_fetch import HttpFetchResponse, fetch_http, validate_http_url
|
||||
|
||||
|
||||
DVELOP_D3_PROVIDER_ID = "dvelop_d3"
|
||||
DVELOP_D3_ARCHIVE_PROFILE = "dvelop-d3-record-transfer-v1"
|
||||
DVELOP_D3_EXTERNAL_PROVIDER_ID = "dms.dvelop_d3"
|
||||
MAX_D3_RESPONSE_BYTES = 4 * 1024 * 1024
|
||||
|
||||
|
||||
class DvelopD3Error(RuntimeError):
|
||||
"""Stable, sanitized d.velop d3 integration error."""
|
||||
|
||||
|
||||
class DvelopD3Transport(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
method: str,
|
||||
headers: Mapping[str, str],
|
||||
body: bytes | None,
|
||||
) -> HttpFetchResponse: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DvelopD3Profile:
|
||||
"""Non-secret tenant binding for one d.velop d3 DMSApp repository."""
|
||||
|
||||
api_base_url: str
|
||||
repository_id: str
|
||||
origin: str
|
||||
source_category: str
|
||||
source_id: str
|
||||
mapping_revision: str
|
||||
credential_ref: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
normalized_base = validate_http_url(self.api_base_url, label="d.velop d3 API base URL")
|
||||
normalized_origin = validate_http_url(self.origin, label="d.velop d3 Origin")
|
||||
origin_parts = urlsplit(normalized_origin)
|
||||
if origin_parts.path not in {"", "/"} or origin_parts.query or origin_parts.fragment:
|
||||
raise ValueError("d.velop d3 Origin must contain only scheme, host, and port.")
|
||||
for name in (
|
||||
"repository_id",
|
||||
"source_category",
|
||||
"source_id",
|
||||
"mapping_revision",
|
||||
"credential_ref",
|
||||
):
|
||||
value = str(getattr(self, name) or "").strip()
|
||||
if not value or len(value) > 255:
|
||||
raise ValueError(f"d.velop d3 {name.replace('_', ' ')} is required and limited to 255 characters.")
|
||||
object.__setattr__(self, name, value)
|
||||
object.__setattr__(self, "api_base_url", normalized_base.rstrip("/"))
|
||||
object.__setattr__(
|
||||
self,
|
||||
"origin",
|
||||
urlunsplit((origin_parts.scheme, origin_parts.netloc, "", "", "")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DvelopD3PreflightResult:
|
||||
repository_id: str
|
||||
observed_at: datetime
|
||||
repository_catalog_sha256: str
|
||||
repository_sha256: str
|
||||
object_definitions_sha256: str
|
||||
mapping_revision: str
|
||||
ready_for_mapping_test: bool
|
||||
dispatch_ready: bool = False
|
||||
limitations: tuple[str, ...] = (
|
||||
"Repository discovery does not prove a configured source mapping.",
|
||||
"Archive custody and recovery conformance require separate target evidence.",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DvelopD3StorePlan:
|
||||
"""Reviewable DMSApp o2m request; executing it is a separate governed effect."""
|
||||
|
||||
repository_id: str
|
||||
endpoint: str
|
||||
origin: str
|
||||
idempotency_key: str
|
||||
body: Mapping[str, object]
|
||||
body_sha256: str
|
||||
mapping_revision: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DvelopD3Client:
|
||||
profile: DvelopD3Profile
|
||||
bearer_token: str | None = field(default=None, repr=False)
|
||||
transport: DvelopD3Transport | None = field(default=None, repr=False)
|
||||
timeout_seconds: int = 30
|
||||
|
||||
def preflight(self) -> DvelopD3PreflightResult:
|
||||
catalog = self._get_json("dms/r")
|
||||
repository = self._get_json(
|
||||
f"dms/r/{quote(self.profile.repository_id, safe='')}"
|
||||
)
|
||||
object_definitions = self._get_json(
|
||||
f"dms/r/{quote(self.profile.repository_id, safe='')}/objdef"
|
||||
)
|
||||
return DvelopD3PreflightResult(
|
||||
repository_id=self.profile.repository_id,
|
||||
observed_at=datetime.now(UTC),
|
||||
repository_catalog_sha256=_json_sha256(catalog),
|
||||
repository_sha256=_json_sha256(repository),
|
||||
object_definitions_sha256=_json_sha256(object_definitions),
|
||||
mapping_revision=self.profile.mapping_revision,
|
||||
ready_for_mapping_test=True,
|
||||
)
|
||||
|
||||
def build_record_store_plan(
|
||||
self,
|
||||
request: RecordArchiveTransferRequest,
|
||||
*,
|
||||
content_location_uri: str,
|
||||
) -> DvelopD3StorePlan:
|
||||
if request.package.profile != DVELOP_D3_ARCHIVE_PROFILE:
|
||||
raise DvelopD3Error("The transfer package does not use the d.velop d3 profile.")
|
||||
content_uri = validate_http_url(
|
||||
content_location_uri,
|
||||
label="d.velop d3 content location URI",
|
||||
)
|
||||
body: dict[str, object] = {
|
||||
"sourceCategory": self.profile.source_category,
|
||||
"sourceId": self.profile.source_id,
|
||||
"sourceProperties": {
|
||||
"govoplanPackageId": request.package.package_id,
|
||||
"govoplanRecordId": request.package.record_id,
|
||||
"govoplanRecordRevision": str(request.package.record_revision),
|
||||
"govoplanManifestSha256": request.package.manifest_sha256,
|
||||
"govoplanPurpose": request.purpose,
|
||||
"govoplanMappingRevision": self.profile.mapping_revision,
|
||||
},
|
||||
"contentLocationUri": content_uri,
|
||||
}
|
||||
encoded = _canonical_json(body)
|
||||
return DvelopD3StorePlan(
|
||||
repository_id=self.profile.repository_id,
|
||||
endpoint=self._url(
|
||||
f"dms/r/{quote(self.profile.repository_id, safe='')}/o2m"
|
||||
),
|
||||
origin=self.profile.origin,
|
||||
idempotency_key=request.idempotency_key,
|
||||
body=body,
|
||||
body_sha256=hashlib.sha256(encoded).hexdigest(),
|
||||
mapping_revision=self.profile.mapping_revision,
|
||||
)
|
||||
|
||||
def _get_json(self, relative_path: str) -> object:
|
||||
response = self._request(relative_path, method="GET", body=None)
|
||||
if response.status != 200:
|
||||
raise DvelopD3Error(
|
||||
f"d.velop d3 discovery returned HTTP {response.status}."
|
||||
)
|
||||
content_type = next(
|
||||
(
|
||||
value
|
||||
for key, value in response.headers.items()
|
||||
if key.casefold() == "content-type"
|
||||
),
|
||||
"",
|
||||
).partition(";")[0].strip().casefold()
|
||||
if content_type not in {"application/json", "application/hal+json"}:
|
||||
raise DvelopD3Error("d.velop d3 discovery did not return JSON or HAL+JSON.")
|
||||
if len(response.body) > MAX_D3_RESPONSE_BYTES:
|
||||
raise DvelopD3Error("d.velop d3 discovery response exceeded the safety limit.")
|
||||
try:
|
||||
return json.loads(response.body)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise DvelopD3Error("d.velop d3 discovery returned malformed JSON.") from exc
|
||||
|
||||
def _request(
|
||||
self,
|
||||
relative_path: str,
|
||||
*,
|
||||
method: str,
|
||||
body: bytes | None,
|
||||
) -> HttpFetchResponse:
|
||||
url = self._url(relative_path)
|
||||
headers = {
|
||||
"Accept": "application/hal+json, application/json",
|
||||
"Origin": self.profile.origin,
|
||||
}
|
||||
if self.bearer_token:
|
||||
headers["Authorization"] = f"Bearer {self.bearer_token}"
|
||||
if self.transport is not None:
|
||||
return self.transport(url, method=method, headers=headers, body=body)
|
||||
if not self.bearer_token:
|
||||
raise DvelopD3Error(
|
||||
"d.velop d3 authentication is unavailable; resolve the configured credential envelope first."
|
||||
)
|
||||
return fetch_http(
|
||||
url,
|
||||
method=method,
|
||||
headers=headers,
|
||||
body=body,
|
||||
timeout=self.timeout_seconds,
|
||||
max_bytes=MAX_D3_RESPONSE_BYTES,
|
||||
label="d.velop d3 DMSApp",
|
||||
redirect_sensitive_headers=("Authorization",),
|
||||
)
|
||||
|
||||
def _url(self, relative_path: str) -> str:
|
||||
url = urljoin(f"{self.profile.api_base_url}/", relative_path.lstrip("/"))
|
||||
base = urlsplit(self.profile.api_base_url)
|
||||
candidate = urlsplit(url)
|
||||
if _origin(base) != _origin(candidate):
|
||||
raise DvelopD3Error("d.velop d3 endpoint escaped the configured API origin.")
|
||||
return url
|
||||
|
||||
|
||||
class UnconfiguredDvelopD3ArchiveProvider:
|
||||
"""Advertise the real-product profile without enabling an untested effect."""
|
||||
|
||||
provider_id = DVELOP_D3_PROVIDER_ID
|
||||
|
||||
def state(self) -> RecordArchiveProviderState:
|
||||
return RecordArchiveProviderState(
|
||||
provider_id=self.provider_id,
|
||||
label="d.velop d3 DMS",
|
||||
profiles=(DVELOP_D3_ARCHIVE_PROFILE,),
|
||||
authority_modes=("external_authoritative",),
|
||||
healthy=False,
|
||||
checked_at=datetime.now(UTC),
|
||||
limitations=(
|
||||
"A tenant repository, credential envelope, source mapping, and target test are required.",
|
||||
"Records dispatch remains disabled until custody and recovery are evidenced.",
|
||||
),
|
||||
simulated=False,
|
||||
)
|
||||
|
||||
def dispatch(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: RecordArchiveTransferRequest,
|
||||
) -> RecordArchiveReceipt:
|
||||
del session, principal, request
|
||||
raise RecordContractError(
|
||||
"d.velop d3 dispatch is disabled until the configured repository passes mapping, custody, and recovery target tests."
|
||||
)
|
||||
|
||||
|
||||
def _origin(parts) -> tuple[str, str, int | None]:
|
||||
return (
|
||||
parts.scheme.casefold(),
|
||||
(parts.hostname or "").casefold(),
|
||||
parts.port or (443 if parts.scheme.casefold() == "https" else 80),
|
||||
)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
def _json_sha256(value: object) -> str:
|
||||
return hashlib.sha256(_canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DVELOP_D3_ARCHIVE_PROFILE",
|
||||
"DVELOP_D3_EXTERNAL_PROVIDER_ID",
|
||||
"DVELOP_D3_PROVIDER_ID",
|
||||
"DvelopD3Client",
|
||||
"DvelopD3Error",
|
||||
"DvelopD3PreflightResult",
|
||||
"DvelopD3Profile",
|
||||
"DvelopD3StorePlan",
|
||||
"UnconfiguredDvelopD3ArchiveProvider",
|
||||
]
|
||||
@@ -0,0 +1,247 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from govoplan_core.core.access import (
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
)
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationCondition,
|
||||
DocumentationLink,
|
||||
DocumentationTopic,
|
||||
ModuleContext,
|
||||
ModuleManifest,
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderDeclaration,
|
||||
ExternalProviderRuntimeState,
|
||||
ExternalProviderStateContext,
|
||||
ExternalProviderStateProviderRegistration,
|
||||
ProviderBehaviorDeclaration,
|
||||
ProviderObjectDeclaration,
|
||||
declared_module_architecture,
|
||||
)
|
||||
from govoplan_core.core.records import record_archive_capability
|
||||
from govoplan_dms.backend.dvelop_d3 import (
|
||||
DVELOP_D3_EXTERNAL_PROVIDER_ID,
|
||||
DVELOP_D3_PROVIDER_ID,
|
||||
UnconfiguredDvelopD3ArchiveProvider,
|
||||
)
|
||||
|
||||
|
||||
MODULE_ID = "dms"
|
||||
MODULE_VERSION = "0.1.19"
|
||||
READ_SCOPE = "dms:integration:read"
|
||||
ADMIN_SCOPE = "dms:integration:admin"
|
||||
D3_ARCHIVE_CAPABILITY = record_archive_capability(DVELOP_D3_PROVIDER_ID)
|
||||
|
||||
|
||||
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="DMS",
|
||||
level="tenant",
|
||||
module_id=module_id,
|
||||
resource=resource,
|
||||
action=action,
|
||||
)
|
||||
|
||||
|
||||
D3_PROVIDER = ExternalProviderDeclaration(
|
||||
id=DVELOP_D3_EXTERNAL_PROVIDER_ID,
|
||||
module_id=MODULE_ID,
|
||||
label="d.velop d3 DMSApp repository",
|
||||
maturity="read",
|
||||
operations=("discover", "read", "preview", "dry_run"),
|
||||
objects=(
|
||||
ProviderObjectDeclaration(
|
||||
object_type="repository",
|
||||
field_groups=("identity", "object_definitions", "source_mapping"),
|
||||
authority_modes=("external_authoritative", "linked_reference"),
|
||||
default_authority_mode="external_authoritative",
|
||||
),
|
||||
ProviderObjectDeclaration(
|
||||
object_type="record_transfer",
|
||||
field_groups=("manifest", "content_location", "mapping_revision"),
|
||||
authority_modes=("external_authoritative",),
|
||||
default_authority_mode="external_authoritative",
|
||||
),
|
||||
),
|
||||
behavior=ProviderBehaviorDeclaration(
|
||||
revision_tokens="Repository and object-definition digests plus the configured mapping revision are retained.",
|
||||
concurrency="A reviewed plan is bound to the exact GovOPlaN manifest and mapping revision.",
|
||||
freshness="Repository discovery records its observation time and response digests.",
|
||||
health="Discovery, authentication, repository lookup, object definitions, mapping, custody, and recovery are distinct gates.",
|
||||
max_read_items=1000,
|
||||
idempotency="The Records package id and manifest digest form the target correlation key.",
|
||||
retry="Discovery is retryable; writes remain disabled until target-specific reconciliation is proven.",
|
||||
timeout_seconds=30,
|
||||
conflicts="Mapping changes invalidate an earlier store plan and require a new review.",
|
||||
outcome_unknown="A timed-out write must be reconciled by package correlation before retry.",
|
||||
outcome_unknown_supported=True,
|
||||
evidence="Endpoint, repository, mapping revision, response digests, package digest, and target receipts are evidence; secrets are excluded.",
|
||||
correction="Correct repository or mapping configuration and produce a newly digest-bound plan.",
|
||||
rollback="Repository effects are not assumed to be transactionally reversible.",
|
||||
compensation="Target-specific recovery may supersede or quarantine an erroneous object after reconciliation.",
|
||||
reconciliation="Look up the stable package correlation and compare the exact manifest digest before any retry.",
|
||||
outage="Records retains its prepared package and never infers custody from an unavailable target.",
|
||||
classifications=("confidential", "restricted"),
|
||||
purposes=("document management", "governed record transfer"),
|
||||
retention="Records and target DMS retention policies remain explicit and independently evidenced.",
|
||||
secret_handling="Only credential-envelope references are configured; bearer values are never persisted or returned.",
|
||||
),
|
||||
capability_names=(D3_ARCHIVE_CAPABILITY,),
|
||||
documentation_topic_ids=("dms.dvelop-d3",),
|
||||
)
|
||||
|
||||
|
||||
def _d3_archive_provider(context: ModuleContext) -> UnconfiguredDvelopD3ArchiveProvider:
|
||||
del context
|
||||
return UnconfiguredDvelopD3ArchiveProvider()
|
||||
|
||||
|
||||
def _d3_provider_states(
|
||||
context: ExternalProviderStateContext,
|
||||
) -> tuple[ExternalProviderRuntimeState, ...]:
|
||||
del context
|
||||
return (
|
||||
ExternalProviderRuntimeState(
|
||||
provider_id=DVELOP_D3_EXTERNAL_PROVIDER_ID,
|
||||
observed_at=datetime.now(UTC),
|
||||
configured=False,
|
||||
active=False,
|
||||
health="inactive",
|
||||
freshness="not_applicable",
|
||||
conflict="not_applicable",
|
||||
recovery="unsupported",
|
||||
detail="No target-tested d.velop d3 tenant binding is configured; dispatch is disabled.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id=MODULE_ID,
|
||||
name="DMS",
|
||||
version=MODULE_VERSION,
|
||||
dependencies=("access",),
|
||||
optional_dependencies=("records", "files", "audit", "policy"),
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
),
|
||||
permissions=(
|
||||
_permission(READ_SCOPE, "View DMS integration", "Inspect configured DMS bindings and non-secret target health."),
|
||||
_permission(ADMIN_SCOPE, "Administer DMS integration", "Configure and test tenant DMS repositories, mappings, and recovery evidence."),
|
||||
),
|
||||
role_templates=(
|
||||
RoleTemplate(
|
||||
slug="dms_integration_administrator",
|
||||
name="DMS integration administrator",
|
||||
description="Configure and verify governed external document-management bindings.",
|
||||
permissions=(READ_SCOPE, ADMIN_SCOPE),
|
||||
),
|
||||
),
|
||||
capability_factories={D3_ARCHIVE_CAPABILITY: _d3_archive_provider},
|
||||
external_providers=(D3_PROVIDER,),
|
||||
external_provider_state_providers=(
|
||||
ExternalProviderStateProviderRegistration(
|
||||
module_id=MODULE_ID,
|
||||
provider_id=DVELOP_D3_EXTERNAL_PROVIDER_ID,
|
||||
provider=_d3_provider_states,
|
||||
),
|
||||
),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="dms.boundary",
|
||||
title="DMS integration boundary",
|
||||
summary="Connect external document-management products without moving Files storage or Records lifecycle ownership into the connector.",
|
||||
body=(
|
||||
"DMS owns product-specific repository discovery, source mapping, version and reference semantics, and target receipts. Files continues to own binary storage, while Records owns eAkte filing, retention, holds, disposition, and archive handoff. A provider declaration is not proof that a configured target is healthy or conformant."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "operator", "module_admin", "records_manager"),
|
||||
related_modules=("files", "records", "access"),
|
||||
links=(DocumentationLink(label="DMS boundary", href="docs/DMS_BOUNDARY.md", kind="repository"),),
|
||||
translations={
|
||||
"de": {
|
||||
"title": "Integrationsgrenze des DMS-Moduls",
|
||||
"summary": "Externe Dokumentenmanagementprodukte anbinden, ohne Dateiablage oder Aktenlebenszyklus in den Konnektor zu verlagern.",
|
||||
"body": "DMS verantwortet produktspezifische Repository-Erkennung, Quellzuordnung, Versions- und Referenzsemantik sowie Zielbelege. Files bleibt für Binärdaten zuständig; Records verwaltet Veraktung, Aufbewahrung, Sperren, Aussonderung und Archivübergabe. Eine Anbieterdeklaration beweist weder Gesundheit noch Konformität eines konfigurierten Ziels.",
|
||||
}
|
||||
},
|
||||
order=100,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="dms.dvelop-d3",
|
||||
title="Configure and verify d.velop d3 DMSApp",
|
||||
summary="Discover a tenant repository, bind an administrator-owned source mapping, and review a digest-bound transfer plan before effects are enabled.",
|
||||
body=(
|
||||
"Configure the HTTPS API base, repository id, calling Origin, d3 source category and source id, mapping revision, and a reusable Access credential envelope. Preflight reads the repository catalog, selected repository, and object definitions through DMSApp and retains only response digests. A store plan binds the exact Records package, content location, purpose, idempotency key, and mapping revision. Dispatch stays unavailable until a real target test proves authentication, source mapping, correlation lookup, custody receipt, unknown-outcome reconciliation, and recovery."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("operator", "module_admin", "records_manager", "auditor"),
|
||||
related_modules=("records", "files", "access", "audit"),
|
||||
conditions=(
|
||||
DocumentationCondition(any_scopes=(READ_SCOPE, ADMIN_SCOPE)),
|
||||
),
|
||||
links=(DocumentationLink(label="d.velop d3 integration profile", href="docs/DVELOP_D3_INTEGRATION.md", kind="repository"),),
|
||||
translations={
|
||||
"de": {
|
||||
"title": "d.velop d3 DMSApp konfigurieren und prüfen",
|
||||
"summary": "Ein Mandanten-Repository erkennen, eine administrativ verantwortete Quellzuordnung binden und vor Wirkungen einen prüfsummengebundenen Übergabeplan kontrollieren.",
|
||||
"body": "Konfigurieren Sie HTTPS-API-Basis, Repository-ID, aufrufenden Origin, d3-Quellkategorie und -Quell-ID, Mapping-Revision sowie einen wiederverwendbaren Access-Berechtigungsnachweis. Der Vorabtest liest Repository-Katalog, ausgewähltes Repository und Objektdefinitionen über DMSApp und bewahrt nur Antwortprüfsummen. Ein Ablageplan bindet das exakte Records-Paket, die Inhaltsadresse, den Zweck, den Idempotenzschlüssel und die Mapping-Revision. Die Übergabe bleibt gesperrt, bis ein echter Zieltest Authentifizierung, Quellzuordnung, Korrelationssuche, Verwahrungsbeleg, Abgleich unbekannter Ergebnisse und Wiederherstellung nachweist.",
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"prerequisites": [
|
||||
"A d.velop d3 tenant and DMSApp endpoint are available.",
|
||||
"The tenant administrator has created and reviewed the source mapping.",
|
||||
"Authentication is held in a scoped Access credential envelope.",
|
||||
],
|
||||
"steps": [
|
||||
"Run repository and object-definition preflight.",
|
||||
"Review the configured source category, source id, and mapping revision.",
|
||||
"Build and compare the digest-bound store plan without executing it.",
|
||||
"Complete target custody, reconciliation, and recovery evidence before enabling dispatch.",
|
||||
],
|
||||
"limitations": [
|
||||
"This release does not enable real Records dispatch.",
|
||||
"Repository discovery alone does not prove source mapping or archival custody.",
|
||||
],
|
||||
"consequences": [
|
||||
"Changing the mapping revision invalidates an earlier plan.",
|
||||
"A timeout never implies success and requires target reconciliation.",
|
||||
"No secret is included in plans, diagnostics, or retained evidence.",
|
||||
],
|
||||
},
|
||||
order=110,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="data_reporting_integration",
|
||||
kind="integration",
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="docs/DVELOP_D3_INTEGRATION.md",
|
||||
test_ref="tests/test_dvelop_d3.py",
|
||||
known_limits=("Real d.velop d3 writes require configured-target conformance and recovery evidence.",),
|
||||
supported_authority_modes=("external_authoritative", "linked_reference"),
|
||||
owned_concepts=("DMS product binding", "DMS source mapping", "DMS target receipt"),
|
||||
non_owned_concepts=("file blob", "record lifecycle", "archive disposition"),
|
||||
recovery_docs=("docs/DVELOP_D3_INTEGRATION.md",),
|
||||
security_docs=("docs/DVELOP_D3_INTEGRATION.md",),
|
||||
operations_docs=("docs/DVELOP_D3_INTEGRATION.md",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_manifest() -> ModuleManifest:
|
||||
return manifest
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user