feat(core): add provider-neutral records filing contract

This commit is contained in:
2026-08-06 01:43:08 +02:00
parent 7ea0cb8655
commit 5d1287735e
9 changed files with 380 additions and 2 deletions
+201
View File
@@ -0,0 +1,201 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable
CAPABILITY_RECORDS_FILING = "records.filing"
CAPABILITY_RECORD_SOURCE_PREFIX = "records.source."
RecordSourceAuthority = Literal[
"native_authoritative",
"external_authoritative",
"external_mirror",
"governed_sync",
"governance_overlay",
"linked_reference",
]
class RecordContractError(ValueError):
"""Stable error for provider-neutral record filing operations."""
@dataclass(frozen=True, slots=True)
class RecordSourceLocator:
"""Exact source revision requested for filing into a record."""
tenant_id: str
source_module: str
resource_type: str
resource_id: str
source_revision: str
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text_fields(
self,
"tenant_id",
"source_module",
"resource_type",
"resource_id",
"source_revision",
)
if len(self.resource_id) > 500 or len(self.source_revision) > 255:
raise RecordContractError("Record source identity is too long.")
@dataclass(frozen=True, slots=True)
class RecordSourceReference:
"""Provider-resolved immutable source metadata safe to preserve in Records."""
locator: RecordSourceLocator
label: str
authority_mode: RecordSourceAuthority = "linked_reference"
content_sha256: str | None = None
content_type: str | None = None
size_bytes: int | None = None
valid_from: datetime | None = None
valid_to: datetime | None = None
recorded_at: datetime | None = None
launch_url: str | None = None
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.label.strip():
raise RecordContractError("Record source references require a label.")
if len(self.label) > 500:
raise RecordContractError(
"Record source labels are limited to 500 characters."
)
if self.size_bytes is not None and self.size_bytes < 0:
raise RecordContractError("Record source sizes cannot be negative.")
if self.content_sha256 is not None:
digest = self.content_sha256.removeprefix("sha256:")
if len(digest) != 64 or any(
character not in "0123456789abcdefABCDEF" for character in digest
):
raise RecordContractError(
"Record source SHA-256 digests must be hexadecimal."
)
if self.valid_from and self.valid_to and self.valid_to <= self.valid_from:
raise RecordContractError(
"Record source valid_to must be after valid_from."
)
@dataclass(frozen=True, slots=True)
class RecordFilingRequest:
tenant_id: str
record_id: str
source: RecordSourceLocator
purpose: str
filing_reason: str
idempotency_key: str
volume_id: str | None = None
relationship: str = "contains"
institutional_context: Mapping[str, object] = field(default_factory=dict)
metadata: Mapping[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
_require_text_fields(
self,
"tenant_id",
"record_id",
"purpose",
"filing_reason",
"idempotency_key",
"relationship",
)
if self.source.tenant_id != self.tenant_id:
raise RecordContractError("Record filing cannot cross tenants.")
if len(self.purpose) > 255 or len(self.filing_reason) > 2_000:
raise RecordContractError("Record filing purpose or reason is too long.")
if len(self.idempotency_key) > 255:
raise RecordContractError(
"Record filing idempotency keys are limited to 255 characters."
)
@dataclass(frozen=True, slots=True)
class RecordFilingResult:
record_id: str
item_id: str
sequence: int
source: RecordSourceReference
filed_at: datetime
replayed: bool = False
@runtime_checkable
class RecordSourceProvider(Protocol):
provider_id: str
def resource_types(self) -> Sequence[str]: ...
def resolve(
self,
session: object,
principal: object,
*,
locator: RecordSourceLocator,
purpose: str,
) -> RecordSourceReference:
"""Resolve one currently authorized, exact source revision."""
@runtime_checkable
class RecordFilingService(Protocol):
def file(
self,
session: object,
principal: object,
*,
request: RecordFilingRequest,
) -> RecordFilingResult: ...
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.")
return f"{CAPABILITY_RECORD_SOURCE_PREFIX}{normalized}"
def record_source_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_SOURCE_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():
raise RecordContractError(
f"Record contract field {field_name} is required."
)
__all__ = [
"CAPABILITY_RECORDS_FILING",
"CAPABILITY_RECORD_SOURCE_PREFIX",
"RecordContractError",
"RecordFilingRequest",
"RecordFilingResult",
"RecordFilingService",
"RecordSourceAuthority",
"RecordSourceLocator",
"RecordSourceProvider",
"RecordSourceReference",
"record_source_capabilities",
"record_source_capability",
]