104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""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"]
|