Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions reflexio/server/services/playbook/publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
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.
# Mirrors user_playbook_deployment_lifecycles_terminal_reason_check, which is
# now NINE values wide (20260827070000). 'governed_erasure' is the live
# governance erase path's, and 'observed_regression' and
# 'confirmed_online_support' are Phase 6's two opposite outcomes; all are
# accepted here because a terminal tuple READ BACK from an idempotent replay may
# legitimately carry any of them. The restoration RPC accepts a strictly
# narrower set, and the confirm RPC accepts exactly one.
LIFECYCLE_TERMINAL_REASONS: frozenset[str] = frozenset(
{
"insufficient_online_support",
Expand All @@ -36,6 +38,7 @@
"stale_incumbent",
"observed_regression",
"governed_erasure",
"confirmed_online_support",
}
)
PublishableOptimizerKind = Literal[
Expand Down Expand Up @@ -658,6 +661,29 @@ def restore_user_playbook_provisional_publication(
"""Reselect the retained predecessor and terminalize, under a fence."""
...

def confirm_user_playbook_provisional_publication(
self,
*,
lifecycle_id: int,
expected_fence: int,
expected_successor_fingerprint: str,
support_session_count: int,
refute_session_count: int,
global_coverage_numerator: int,
global_coverage_denominator: int,
target_coverage_numerator: int,
target_coverage_denominator: int,
) -> LifecycleTerminalResult:
"""Keep the successor and terminalize as confirmed, under a fence.

No ``reason`` parameter: ``confirmed_online_support`` is the only reason
this transition can record, so passing it would create a second place
the pairing could drift. The six counts are the evidence the boundary
decided on; the RPC re-checks the arithmetic itself and refuses an
inadmissible set rather than obeying it.
"""
...

def displace_user_playbook_provisional_publication(
self, *, lifecycle_id: int
) -> LifecycleTerminalResult:
Expand Down
26 changes: 26 additions & 0 deletions reflexio/server/services/storage/storage_base/playbook/_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ def restore_user_playbook_provisional_publication(
"Storage backend does not support provisional user-playbook restoration"
)

def confirm_user_playbook_provisional_publication(
self,
*,
lifecycle_id: int,
expected_fence: int,
expected_successor_fingerprint: str,
support_session_count: int,
refute_session_count: int,
global_coverage_numerator: int,
global_coverage_denominator: int,
target_coverage_numerator: int,
target_coverage_denominator: int,
) -> "LifecycleTerminalResult":
"""Atomically keep the successor and terminalize as confirmed.

Declared here, alongside restore and displace, rather than as an
enterprise-only extra: the three are one termination triple over the
same lifecycle row, and their shared Protocol
(``UserPlaybookLifecycleTerminationStore``) lives in the OSS package. A
backend that satisfied two thirds of it would be a surface every reader
has to special-case.
"""
raise NotImplementedError(
"Storage backend does not support provisional user-playbook confirmation"
)

def displace_user_playbook_provisional_publication(
self, *, lifecycle_id: int
) -> "LifecycleTerminalResult":
Expand Down
18 changes: 18 additions & 0 deletions tests/server/services/playbook/test_publication_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
UserPlaybook,
)
from reflexio.server.services.playbook.publication import (
LIFECYCLE_TERMINAL_REASONS,
LIFECYCLE_TERMINAL_STATES,
DecisionProofEnvelope,
LifecycleTerminalResult,
Expand Down Expand Up @@ -57,6 +58,23 @@ def test_lifecycle_terminal_result_is_derived_from_the_state_literal() -> None:
terminal_reason="not_a_reason",
terminal_at=1_700_000_000,
)
# Phase 6 made 'confirmed' representable: the SQL CHECK
# (20260827070000) admits 'confirmed_online_support', so a terminal tuple
# read back from an idempotent replay may carry it and this set must too.
confirmed = LifecycleTerminalResult(
lifecycle_id=1,
state="confirmed",
terminal_reason="confirmed_online_support",
terminal_at=1,
)
assert confirmed.terminal_reason in LIFECYCLE_TERMINAL_REASONS
with pytest.raises(ValueError, match="reason is not enumerated"):
LifecycleTerminalResult(
lifecycle_id=1,
state="confirmed",
terminal_reason="confirmed_because_i_said_so",
terminal_at=1,
)


def test_open_world_publication_literals_and_user_playbook_field_partition() -> None:
Expand Down
Loading