File size: 6,356 Bytes
5e0b58b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""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)