From 16fec7bbab00e1321216a74c934fef89af8108fe Mon Sep 17 00:00:00 2001 From: Guangyu Date: Sat, 29 Aug 2026 21:56:17 +0000 Subject: [PATCH 1/2] feat(offline-tuner): add the lifecycle termination storage seam Adds LifecycleTerminalResult -- the durable terminal tuple of one provisional deployment lifecycle -- plus the two storage seam methods Phase 5 restoration and displacement call. The lifecycle row IS the result, so repeat delivery reads the same row back rather than needing a second results table. The terminal-state set is DERIVED from the exported OpenWorldDeploymentLifecycleState Literal instead of being restated, so the SQL state CHECK, the Literal and the Python set cannot drift into three different answers. That Literal previously had zero consumers. --- .../server/services/playbook/publication.py | 83 ++++++++++++++++++- .../storage/storage_base/playbook/_user.py | 22 +++++ .../playbook/test_publication_models.py | 37 +++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/reflexio/server/services/playbook/publication.py b/reflexio/server/services/playbook/publication.py index e0c47418..c129a6ec 100644 --- a/reflexio/server/services/playbook/publication.py +++ b/reflexio/server/services/playbook/publication.py @@ -7,11 +7,37 @@ from collections.abc import Mapping from dataclasses import dataclass from hashlib import sha256 -from typing import Literal, Protocol +from typing import Literal, Protocol, get_args -from reflexio.models.api_schema.domain.entities import OptimizerKind, UserPlaybook +from reflexio.models.api_schema.domain.entities import ( + OpenWorldDeploymentLifecycleState, + OptimizerKind, + UserPlaybook, +) PublicationOutcome = Literal["applied", "incumbent_changed"] +# Derived from the exported Literal rather than restated, so the SQL state +# CHECK, the Literal, and this set cannot drift into three different answers. +LIFECYCLE_TERMINAL_STATES: frozenset[str] = frozenset( + get_args(OpenWorldDeploymentLifecycleState) +) - {"provisional"} +# Mirrors user_playbook_deployment_lifecycles_terminal_reason_check +# (20260827040000). 'observed_regression' is Phase 6's and 'governed_erasure' +# is the live governance erase path's; both are accepted here because a +# terminal tuple READ BACK from an idempotent replay may legitimately carry +# either. The restoration RPC itself accepts a strictly narrower set. +LIFECYCLE_TERMINAL_REASONS: frozenset[str] = frozenset( + { + "insufficient_online_support", + "analyst_unqualified", + "confirmation_capability_invalidated", + "governance_invalidated", + "tuner_disabled", + "stale_incumbent", + "observed_regression", + "governed_erasure", + } +) PublishableOptimizerKind = Literal[ "gepa", "offline_tuner_replay", "offline_tuner_open_world" ] @@ -506,6 +532,38 @@ def __post_init__(self) -> None: ) +@dataclass(frozen=True) +class LifecycleTerminalResult: + """The durable terminal tuple of one provisional deployment lifecycle. + + There is no separate results table: the lifecycle row itself is the result. + ``state``/``terminal_reason``/``terminal_at`` are already CHECK-coupled in + the tenant schema, so repeat delivery of a restoration or displacement + reads the same row back and returns the identical tuple. + + Args: + lifecycle_id (int): ``deployment_lifecycle_id`` of the terminalized row. + state (str): One of the four non-provisional lifecycle states. + terminal_reason (str): The enumerated reason the successor was pulled. + terminal_at (int): Epoch second the transition committed. + """ + + lifecycle_id: int + state: str + terminal_reason: str + terminal_at: int + + def __post_init__(self) -> None: + if type(self.lifecycle_id) is not int or self.lifecycle_id <= 0: + raise ValueError("lifecycle terminal result id must be positive") + if self.state not in LIFECYCLE_TERMINAL_STATES: + raise ValueError("lifecycle terminal result state is not terminal") + if self.terminal_reason not in LIFECYCLE_TERMINAL_REASONS: + raise ValueError("lifecycle terminal result reason is not enumerated") + if type(self.terminal_at) is not int or self.terminal_at <= 0: + raise ValueError("lifecycle terminal result timestamp must be positive") + + @dataclass(frozen=True) class PublicationResult: job_id: int @@ -586,6 +644,27 @@ def load_user_playbook_provisional_publication_result( ) -> ProvisionalPublicationResult | None: ... +class UserPlaybookLifecycleTerminationStore(Protocol): + """Durable Phase 5 termination of a provisional deployment lifecycle.""" + + def restore_user_playbook_provisional_publication( + self, + *, + lifecycle_id: int, + reason: str, + expected_fence: int, + expected_successor_fingerprint: str, + ) -> LifecycleTerminalResult: + """Reselect the retained predecessor and terminalize, under a fence.""" + ... + + def displace_user_playbook_provisional_publication( + self, *, lifecycle_id: int + ) -> LifecycleTerminalResult: + """Terminalize as displaced/stale_incumbent; the manual version wins.""" + ... + + class UserPlaybookPublicationService: """Coordinates proof verification with durable staging and atomic commit.""" diff --git a/reflexio/server/services/storage/storage_base/playbook/_user.py b/reflexio/server/services/storage/storage_base/playbook/_user.py index f8e25dc4..166959ab 100644 --- a/reflexio/server/services/storage/storage_base/playbook/_user.py +++ b/reflexio/server/services/storage/storage_base/playbook/_user.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from reflexio.server.services.playbook.publication import ( + LifecycleTerminalResult, ProvisionalPublicationRequest, ProvisionalPublicationResult, PublicationClaim, @@ -107,6 +108,27 @@ def load_user_playbook_provisional_publication_result( "Storage backend does not support provisional user-playbook publication" ) + def restore_user_playbook_provisional_publication( + self, + *, + lifecycle_id: int, + reason: str, + expected_fence: int, + expected_successor_fingerprint: str, + ) -> "LifecycleTerminalResult": + """Atomically restore the retained predecessor and terminalize.""" + raise NotImplementedError( + "Storage backend does not support provisional user-playbook restoration" + ) + + def displace_user_playbook_provisional_publication( + self, *, lifecycle_id: int + ) -> "LifecycleTerminalResult": + """Terminalize as displaced so a manual edit may proceed.""" + raise NotImplementedError( + "Storage backend does not support provisional user-playbook restoration" + ) + @abstractmethod def save_user_playbooks( self, diff --git a/tests/server/services/playbook/test_publication_models.py b/tests/server/services/playbook/test_publication_models.py index 228f9a62..61622a17 100644 --- a/tests/server/services/playbook/test_publication_models.py +++ b/tests/server/services/playbook/test_publication_models.py @@ -11,7 +11,9 @@ UserPlaybook, ) from reflexio.server.services.playbook.publication import ( + LIFECYCLE_TERMINAL_STATES, DecisionProofEnvelope, + LifecycleTerminalResult, PublicationClaim, PublicationRequest, PublicationSearchProjection, @@ -22,6 +24,41 @@ ) +def test_lifecycle_terminal_result_is_derived_from_the_state_literal() -> None: + """The terminal set must be DERIVED, not restated. + + ``OpenWorldDeploymentLifecycleState``, the SQL state CHECK and this set are + three statements of one fact; restating it here would let them drift into + three different answers. ``provisional`` is the only non-terminal state, so + a terminal result carrying it is rejected. + """ + assert ( + set(get_args(OpenWorldDeploymentLifecycleState)) - {"provisional"} + == LIFECYCLE_TERMINAL_STATES + ) + restored = LifecycleTerminalResult( + lifecycle_id=7, + state="restored", + terminal_reason="insufficient_online_support", + terminal_at=1_700_000_000, + ) + assert restored.state in LIFECYCLE_TERMINAL_STATES + with pytest.raises(ValueError, match="state is not terminal"): + LifecycleTerminalResult( + lifecycle_id=7, + state="provisional", + terminal_reason="insufficient_online_support", + terminal_at=1_700_000_000, + ) + with pytest.raises(ValueError, match="reason is not enumerated"): + LifecycleTerminalResult( + lifecycle_id=7, + state="restored", + terminal_reason="not_a_reason", + terminal_at=1_700_000_000, + ) + + def test_open_world_publication_literals_and_user_playbook_field_partition() -> None: assert get_args(PublishableOptimizerKind) == ( "gepa", From 5665f2ae42083e54e86a70aaaa950f83aff80383 Mon Sep 17 00:00:00 2001 From: Guangyu Date: Mon, 31 Aug 2026 23:00:20 +0000 Subject: [PATCH 2/2] fix(storage): name displacement in the displace stub's error The displace_user_playbook_provisional_publication stub reported 'provisional user-playbook restoration', pointing a caller on an unsupported backend at the wrong operation. The sibling stubs name their capability family accurately; this one named a different one. --- reflexio/server/services/storage/storage_base/playbook/_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflexio/server/services/storage/storage_base/playbook/_user.py b/reflexio/server/services/storage/storage_base/playbook/_user.py index 166959ab..e0dec61c 100644 --- a/reflexio/server/services/storage/storage_base/playbook/_user.py +++ b/reflexio/server/services/storage/storage_base/playbook/_user.py @@ -126,7 +126,7 @@ def displace_user_playbook_provisional_publication( ) -> "LifecycleTerminalResult": """Terminalize as displaced so a manual edit may proceed.""" raise NotImplementedError( - "Storage backend does not support provisional user-playbook restoration" + "Storage backend does not support provisional user-playbook displacement" ) @abstractmethod