feat(sync): add edge artifact activation ledger

This commit is contained in:
Shay 2026-05-29 19:17:52 -07:00
parent da5c2a9e76
commit f645dc2bdb

193
core/sync/activation.py Normal file
View file

@ -0,0 +1,193 @@
"""Activation ledger for verified edge/cloud sync artifacts.
Activation is separate from download, validation, signature integrity, and
epistemic admissibility. This module is pure and performs no object-store I/O.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from core.sync.artifacts import ArtifactType
from core.sync.manifest import ManifestCheck, SyncManifest
@dataclass(frozen=True, slots=True)
class ActivationRecord:
"""Audit metadata for one activation decision."""
artifact_id: str
artifact_type: ArtifactType
artifact_version: str | None
content_digest: str | None
previous_artifact_id: str | None
active_artifact_id: str | None
reason: str
activated: bool
default_epistemic_status: str
admissible_as_evidence: bool
@dataclass(frozen=True, slots=True)
class ActivationDecision:
"""Result of an activation attempt."""
activated: bool
reason: str
previous_artifact_id: str | None = None
active_artifact_id: str | None = None
audit_record: ActivationRecord | None = None
@dataclass(slots=True)
class ActivationLedger:
"""Tracks active runtime-affecting artifacts by artifact class.
The ledger only activates manifests that already passed validation. It does
not interpret artifact contents and does not promote any claim to coherent.
"""
_active: dict[ArtifactType, SyncManifest] = field(default_factory=dict)
_audit_log: list[ActivationRecord] = field(default_factory=list)
def active_for(self, artifact_type: ArtifactType | str) -> SyncManifest | None:
"""Return the active manifest for an artifact class, if any."""
normalized = artifact_type if isinstance(artifact_type, ArtifactType) else ArtifactType(artifact_type)
return self._active.get(normalized)
@property
def audit_log(self) -> tuple[ActivationRecord, ...]:
"""Immutable view of activation audit records."""
return tuple(self._audit_log)
def activate(self, check: ManifestCheck) -> ActivationDecision:
"""Activate a validated runtime-affecting manifest.
A successful validation is required, but validation alone does not change
active state. Non-runtime artifacts are intentionally not activated.
"""
if not check.accepted or check.manifest is None:
return ActivationDecision(False, "validation_failed")
manifest = check.manifest
if not manifest.authority.runtime_affecting:
record = self._record_for(
manifest,
activated=False,
reason="not_runtime_affecting",
previous=None,
active=None,
)
self._audit_log.append(record)
return ActivationDecision(
False,
"not_runtime_affecting",
audit_record=record,
)
if manifest.authority.requires_activation is False:
record = self._record_for(
manifest,
activated=False,
reason="activation_not_required",
previous=None,
active=None,
)
self._audit_log.append(record)
return ActivationDecision(
False,
"activation_not_required",
audit_record=record,
)
previous = self._active.get(manifest.artifact_type)
previous_id = previous.artifact_id if previous is not None else None
self._active[manifest.artifact_type] = manifest
record = self._record_for(
manifest,
activated=True,
reason="activated",
previous=previous_id,
active=manifest.artifact_id,
)
self._audit_log.append(record)
return ActivationDecision(
True,
"activated",
previous_artifact_id=previous_id,
active_artifact_id=manifest.artifact_id,
audit_record=record,
)
def rollback(self, manifest: SyncManifest) -> ActivationDecision:
"""Restore a previously verified manifest as the active release.
Rollback still records an activation event and still does not confer
epistemic admissibility.
"""
if not manifest.authority.runtime_affecting:
return ActivationDecision(False, "not_runtime_affecting")
previous = self._active.get(manifest.artifact_type)
previous_id = previous.artifact_id if previous is not None else None
self._active[manifest.artifact_type] = manifest
record = self._record_for(
manifest,
activated=True,
reason="rollback_activated",
previous=previous_id,
active=manifest.artifact_id,
)
self._audit_log.append(record)
return ActivationDecision(
True,
"rollback_activated",
previous_artifact_id=previous_id,
active_artifact_id=manifest.artifact_id,
audit_record=record,
)
def _record_for(
self,
manifest: SyncManifest,
*,
activated: bool,
reason: str,
previous: str | None,
active: str | None,
) -> ActivationRecord:
return ActivationRecord(
artifact_id=manifest.artifact_id,
artifact_type=manifest.artifact_type,
artifact_version=manifest.artifact_version,
content_digest=manifest.content_digest,
previous_artifact_id=previous,
active_artifact_id=active,
reason=reason,
activated=activated,
default_epistemic_status=manifest.default_epistemic_status,
admissible_as_evidence=manifest.admissible_as_evidence,
)
def audit_dicts(self) -> tuple[dict[str, Any], ...]:
"""Return JSON-serializable audit records."""
return tuple(
{
"artifact_id": record.artifact_id,
"artifact_type": record.artifact_type.value,
"artifact_version": record.artifact_version,
"content_digest": record.content_digest,
"previous_artifact_id": record.previous_artifact_id,
"active_artifact_id": record.active_artifact_id,
"reason": record.reason,
"activated": record.activated,
"default_epistemic_status": record.default_epistemic_status,
"admissible_as_evidence": record.admissible_as_evidence,
}
for record in self._audit_log
)