github-actions[bot]
Sync: publish Space API fix
5e0b58b
Raw History Blame Contribute Delete
6.36 kB
"""Coordinator between dry-run proposals and controlled structural mutation."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from threading import RLock
from typing import TYPE_CHECKING
from .approval import ProposalApprovalPolicy
if TYPE_CHECKING:
from src.dashboard.models import JSONValue
from .policy import (
LegacyStructuralProposal,
PolicyReport,
StructuralAction,
StructuralProposal,
)
ProposalExecutor = Callable[[LegacyStructuralProposal], int]
@dataclass(frozen=True, slots=True)
class ProposalDecision:
proposal_id: str
accepted: bool
reason: str
@dataclass(frozen=True, slots=True)
class SelfOrganizationSnapshot:
"""Public coordinator telemetry for the dashboard."""
enabled: bool
dry_run: bool
proposals_seen: int
proposals_executed: int
mutations_applied: int
last_proposal: LegacyStructuralProposal | None
def to_json(self) -> dict[str, JSONValue]:
return {
"enabled": self.enabled,
"dry_run": self.dry_run,
"proposals_seen": self.proposals_seen,
"proposals_executed": self.proposals_executed,
"mutations_applied": self.mutations_applied,
"last_proposal": (
self.last_proposal.to_json() if self.last_proposal is not None else None
),
}
class SelfOrganizationCoordinator:
"""Store latest proposals and require explicit operator approval."""
def __init__(
self,
executor: ProposalExecutor | None = None,
*,
enabled: bool = True,
dry_run: bool = True,
) -> None:
self._lock = RLock()
self._latest: PolicyReport | None = None
self._decisions: list[ProposalDecision] = []
self._executor = executor
self._enabled = enabled
self._dry_run = dry_run
self._proposals_seen = 0
self._proposals_executed = 0
self._mutations_applied = 0
self._last_proposal: LegacyStructuralProposal | None = None
def configure(
self,
*,
enabled: bool | None = None,
dry_run: bool | None = None,
) -> None:
"""Change execution gates without replacing the policy."""
with self._lock:
if enabled is not None:
self._enabled = enabled
if dry_run is not None:
self._dry_run = dry_run
def submit(self, proposal: LegacyStructuralProposal) -> SelfOrganizationSnapshot:
"""Account and optionally execute a legacy structural proposal."""
with self._lock:
self._proposals_seen += 1
self._last_proposal = proposal
if (
not self._enabled
or self._dry_run
or proposal.action is StructuralAction.NONE
):
return self._snapshot_unlocked()
if self._executor is None:
raise RuntimeError(
"Self-organization execution is enabled without an executor."
)
applied = self._executor(proposal)
if applied < 0:
raise ValueError("proposal executor returned a negative mutation count")
self._proposals_executed += 1
self._mutations_applied += applied
return self._snapshot_unlocked()
def snapshot(self) -> SelfOrganizationSnapshot:
"""Return immutable dashboard telemetry."""
with self._lock:
return self._snapshot_unlocked()
def _snapshot_unlocked(self) -> SelfOrganizationSnapshot:
return SelfOrganizationSnapshot(
enabled=self._enabled,
dry_run=self._dry_run,
proposals_seen=self._proposals_seen,
proposals_executed=self._proposals_executed,
mutations_applied=self._mutations_applied,
last_proposal=self._last_proposal,
)
def publish(self, report: PolicyReport) -> None:
with self._lock:
self._latest = report
def latest(self) -> PolicyReport | None:
with self._lock:
return self._latest
def find(self, proposal_id: str) -> StructuralProposal | None:
report = self.latest()
if report is None:
return None
return next((p for p in report.proposals if p.proposal_id == proposal_id), None)
def record_decision(
self, proposal_id: str, accepted: bool, reason: str
) -> ProposalDecision:
decision = ProposalDecision(proposal_id, accepted, reason)
with self._lock:
self._decisions.append(decision)
return decision
def approve(self, proposal_id: str, reason: str = "operator approval") -> bool:
"""Record approval only when the proposal is currently published."""
if self.find(proposal_id) is None:
return False
self.record_decision(proposal_id, True, reason)
return True
def reject(self, proposal_id: str, reason: str = "operator rejection") -> bool:
"""Record rejection only when the proposal is currently published."""
if self.find(proposal_id) is None:
return False
self.record_decision(proposal_id, False, reason)
return True
def auto_process(
self,
policy: ProposalApprovalPolicy,
*,
safety_ok: bool,
cooldown_ok: bool,
kind_allowed: bool,
) -> tuple[ProposalDecision, ...]:
"""Evaluate current proposals without applying structural mutations."""
report = self.latest()
if report is None:
return ()
decisions: list[ProposalDecision] = []
for proposal in report.proposals:
approval = policy.evaluate(
proposal,
safety_ok=safety_ok,
cooldown_ok=cooldown_ok,
kind_allowed=kind_allowed,
)
decisions.append(
self.record_decision(
proposal.proposal_id,
approval.approved,
approval.reason,
)
)
return tuple(decisions)
def decisions(self) -> tuple[ProposalDecision, ...]:
with self._lock:
return tuple(self._decisions)