feat(records): define archive transfer provider contract

This commit is contained in:
2026-08-06 05:36:11 +02:00
parent 5d1287735e
commit f5949427cc
3 changed files with 275 additions and 7 deletions
+174 -6
View File
@@ -8,6 +8,7 @@ from typing import Literal, Protocol, runtime_checkable
CAPABILITY_RECORDS_FILING = "records.filing"
CAPABILITY_RECORD_SOURCE_PREFIX = "records.source."
CAPABILITY_RECORD_ARCHIVE_PREFIX = "records.archive."
RecordSourceAuthority = Literal[
"native_authoritative",
@@ -17,6 +18,17 @@ RecordSourceAuthority = Literal[
"governance_overlay",
"linked_reference",
]
RecordArchiveOutcome = Literal["accepted", "rejected", "outcome_unknown"]
_RECORD_SOURCE_AUTHORITIES = {
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
"governance_overlay",
"linked_reference",
}
_RECORD_ARCHIVE_OUTCOMES = {"accepted", "rejected", "outcome_unknown"}
class RecordContractError(ValueError):
@@ -129,6 +141,109 @@ class RecordFilingResult:
replayed: bool = False
@dataclass(frozen=True, slots=True)
class RecordTransferPackage:
"""Exact, digest-bound package prepared by Records for one provider profile."""
tenant_id: str
package_id: str
record_id: str
record_revision: int
profile: str
manifest_sha256: str
manifest: Mapping[str, object]
def __post_init__(self) -> None:
_require_text_fields(
self,
"tenant_id",
"package_id",
"record_id",
"profile",
"manifest_sha256",
)
if self.record_revision < 1:
raise RecordContractError("Record transfer revisions must be positive.")
_require_sha256(self.manifest_sha256, "Record transfer manifest")
@dataclass(frozen=True, slots=True)
class RecordArchiveProviderState:
provider_id: str
label: str
profiles: tuple[str, ...]
authority_modes: tuple[RecordSourceAuthority, ...]
healthy: bool
checked_at: datetime
last_success_at: datetime | None = None
freshness_seconds: int | None = None
limitations: tuple[str, ...] = ()
simulated: bool = False
def __post_init__(self) -> None:
_require_text_fields(self, "provider_id", "label")
if not self.profiles or any(not item.strip() for item in self.profiles):
raise RecordContractError(
"Record archive providers require at least one profile."
)
if not self.authority_modes:
raise RecordContractError(
"Record archive providers require an authority mode."
)
if any(mode not in _RECORD_SOURCE_AUTHORITIES for mode in self.authority_modes):
raise RecordContractError(
"Record archive providers declared an invalid authority mode."
)
if self.freshness_seconds is not None and self.freshness_seconds < 0:
raise RecordContractError(
"Record archive provider freshness cannot be negative."
)
@dataclass(frozen=True, slots=True)
class RecordArchiveTransferRequest:
package: RecordTransferPackage
purpose: str
idempotency_key: str
institutional_context: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text_fields(self, "purpose", "idempotency_key")
if len(self.purpose) > 255 or len(self.idempotency_key) > 255:
raise RecordContractError(
"Record archive purpose or idempotency key is too long."
)
@dataclass(frozen=True, slots=True)
class RecordArchiveReceipt:
provider_id: str
package_id: str
outcome: RecordArchiveOutcome
observed_at: datetime
receipt_sha256: str
external_reference: str | None = None
retry_safe: bool = False
simulated: bool = False
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text_fields(
self,
"provider_id",
"package_id",
"outcome",
"receipt_sha256",
)
_require_sha256(self.receipt_sha256, "Record archive receipt")
if self.outcome not in _RECORD_ARCHIVE_OUTCOMES:
raise RecordContractError("Record archive receipt outcome is invalid.")
if self.outcome == "outcome_unknown" and self.retry_safe:
raise RecordContractError(
"Unknown archive outcomes cannot be declared retry-safe."
)
@runtime_checkable
class RecordSourceProvider(Protocol):
provider_id: str
@@ -157,16 +272,32 @@ class RecordFilingService(Protocol):
) -> RecordFilingResult: ...
@runtime_checkable
class RecordArchiveProvider(Protocol):
provider_id: str
def state(self) -> RecordArchiveProviderState: ...
def dispatch(
self,
session: object,
principal: object,
*,
request: RecordArchiveTransferRequest,
) -> RecordArchiveReceipt:
"""Dispatch one prepared package without retrying an unknown outcome."""
def record_source_capability(source_module: str) -> str:
normalized = source_module.strip().lower()
if not normalized or any(
character not in "abcdefghijklmnopqrstuvwxyz0123456789_-"
for character in normalized
):
raise RecordContractError("Record source module identifiers are invalid.")
normalized = _capability_suffix(source_module, "source module")
return f"{CAPABILITY_RECORD_SOURCE_PREFIX}{normalized}"
def record_archive_capability(provider_id: str) -> str:
normalized = _capability_suffix(provider_id, "archive provider")
return f"{CAPABILITY_RECORD_ARCHIVE_PREFIX}{normalized}"
def record_source_capabilities(registry: object | None) -> tuple[str, ...]:
if registry is None or not hasattr(registry, "capability_names"):
return ()
@@ -177,6 +308,16 @@ def record_source_capabilities(registry: object | None) -> tuple[str, ...]:
)
def record_archive_capabilities(registry: object | None) -> tuple[str, ...]:
if registry is None or not hasattr(registry, "capability_names"):
return ()
return tuple(
name
for name in registry.capability_names()
if str(name).startswith(CAPABILITY_RECORD_ARCHIVE_PREFIX)
)
def _require_text_fields(value: object, *field_names: str) -> None:
for field_name in field_names:
if not str(getattr(value, field_name, "") or "").strip():
@@ -185,9 +326,33 @@ def _require_text_fields(value: object, *field_names: str) -> None:
)
def _capability_suffix(value: str, label: str) -> str:
normalized = value.strip().lower()
if not normalized or any(
character not in "abcdefghijklmnopqrstuvwxyz0123456789_-"
for character in normalized
):
raise RecordContractError(f"Record {label} identifiers are invalid.")
return normalized
def _require_sha256(value: str, label: str) -> None:
digest = value.removeprefix("sha256:")
if len(digest) != 64 or any(
character not in "0123456789abcdefABCDEF" for character in digest
):
raise RecordContractError(f"{label} SHA-256 must be hexadecimal.")
__all__ = [
"CAPABILITY_RECORD_ARCHIVE_PREFIX",
"CAPABILITY_RECORDS_FILING",
"CAPABILITY_RECORD_SOURCE_PREFIX",
"RecordArchiveOutcome",
"RecordArchiveProvider",
"RecordArchiveProviderState",
"RecordArchiveReceipt",
"RecordArchiveTransferRequest",
"RecordContractError",
"RecordFilingRequest",
"RecordFilingResult",
@@ -196,6 +361,9 @@ __all__ = [
"RecordSourceLocator",
"RecordSourceProvider",
"RecordSourceReference",
"RecordTransferPackage",
"record_archive_capabilities",
"record_archive_capability",
"record_source_capabilities",
"record_source_capability",
]