"""Change acceptance governance for AIOS. Every feature change must provide matching tests. Every architecture change must be reviewed by an independent architecture agent. User-facing changes must be validated by an independent user acceptance agent. """ from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any def utc_now() -> str: return datetime.now(timezone.utc).isoformat() @dataclass(frozen=True) class ChangeDescriptor: change_id: str change_type: str title: str files_changed: list[str] tests: list[str] = field(default_factory=list) user_facing: bool = False architecture_impact: bool = False risk: str = "low" def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(frozen=True) class AcceptanceFinding: check: str status: str detail: str required: bool = True def passed(self) -> bool: return self.status == "pass" or not self.required def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(frozen=True) class AcceptanceDecision: status: str findings: list[AcceptanceFinding] decided_at: str = field(default_factory=utc_now) def to_dict(self) -> dict[str, Any]: return { "status": self.status, "findings": [f.to_dict() for f in self.findings], "decided_at": self.decided_at, } class ChangeAcceptanceGate: def evaluate(self, change: ChangeDescriptor) -> AcceptanceDecision: findings: list[AcceptanceFinding] = [] findings.append(self._tests_required(change)) findings.append(self._user_acceptance_required(change)) findings.append(self._architecture_review_required(change)) findings.append(self._risk_gate(change)) status = "pass" if all(f.passed() for f in findings) else "blocked" return AcceptanceDecision(status, findings) def _tests_required(self, change: ChangeDescriptor) -> AcceptanceFinding: if change.tests: return AcceptanceFinding("tests_present", "pass", "matching tests declared") return AcceptanceFinding("tests_present", "fail", "every feature change requires tests") def _user_acceptance_required(self, change: ChangeDescriptor) -> AcceptanceFinding: if not change.user_facing: return AcceptanceFinding("user_acceptance", "pass", "not user-facing") has_user_test = any("user" in t.lower() or "acceptance" in t.lower() for t in change.tests) return AcceptanceFinding( "user_acceptance", "pass" if has_user_test else "fail", "user-facing change requires user acceptance test", ) def _architecture_review_required(self, change: ChangeDescriptor) -> AcceptanceFinding: if not change.architecture_impact: return AcceptanceFinding("architecture_review", "pass", "no architecture impact") has_arch_test = any("arch" in t.lower() or "architecture" in t.lower() for t in change.tests) return AcceptanceFinding( "architecture_review", "pass" if has_arch_test else "fail", "architecture change requires independent architect review artifact/test", ) def _risk_gate(self, change: ChangeDescriptor) -> AcceptanceFinding: if change.risk in {"low", "medium", "high"}: return AcceptanceFinding("risk_classified", "pass", f"risk={change.risk}") return AcceptanceFinding("risk_classified", "fail", "risk must be low/medium/high") __all__ = ["ChangeDescriptor", "AcceptanceFinding", "AcceptanceDecision", "ChangeAcceptanceGate"]