"""Typed model of the constitution's machine-readable parameters. This is the surface amendments actually modify. The engine reads every operative rule from an instance of :class:`ConstitutionParams`; the harness loads the file from the PR under test, so an amendment that weakens a defense immediately changes engine behavior and trips the corpus. """ from __future__ import annotations from pathlib import Path from typing import Any import yaml from pydantic import BaseModel, ConfigDict, Field class _Section(BaseModel): model_config = ConfigDict(extra="forbid") class DelegationParams(_Section): allowed: bool = True revocable_anytime: bool = True max_chain_depth: int = 1 delegate_cap_fraction: float = 0.1 class VotingParams(_Section): method: str = "one_person_one_vote" quorum: float = 0.5 ordinary_threshold: float = 0.5 kernel_threshold: float = 0.6667 voting_period_days: int = 7 notice_period_days: int = 3 eligibility_delay_days: int = 14 delegation: DelegationParams = Field(default_factory=DelegationParams) class MembershipParams(_Section): admission_threshold: float = 0.5 max_admissions_per_cycle_fraction: float = 0.1 expulsion_threshold: float = 0.6667 expulsion_due_process_days: int = 7 accused_may_vote: bool = True max_expulsions_per_cycle: int = 1 class TreasuryParams(_Section): spend_threshold: float = 0.5 per_proposal_cap_fraction: float = 0.2 per_cycle_cap_fraction: float = 0.34 cycle_days: int = 30 recipient_may_vote: bool = False class EmergencyParams(_Section): declaration_threshold: float = 0.6667 renewal_threshold: float = 0.75 max_duration_days: int = 14 max_renewals: int = 1 redeclaration_cooldown_days: int = 30 powers_available: list[str] = Field(default_factory=lambda: ["expedite_voting"]) expedite_factor: float = 0.5 prohibited_kinds_during: list[str] = Field( default_factory=lambda: ["rule_change_kernel", "expel", "admit"] ) class AmendmentParams(_Section): single_subject: bool = True ratification_delay_days: int = 7 failed_subject_cooldown_days: int = 14 min_decision_threshold: float = 0.5 min_quorum: float = 0.25 kernel_paths: list[str] = Field( default_factory=lambda: [ "voting", "membership", "treasury", "emergency", "amendments", "review", "fork", ] ) invariant_locked_paths: list[str] = Field( default_factory=lambda: [ "voting.method", "voting.delegation.revocable_anytime", "fork.right_to_exit", "review.enabled", "amendments.kernel_paths", "amendments.invariant_locked_paths", "amendments.min_decision_threshold", "amendments.min_quorum", ] ) class ReviewParams(_Section): enabled: bool = True challenge_window_days: int = 7 disparate_impact_ratio: float = 2.0 class ForkParams(_Section): right_to_exit: bool = True exit_share: str = "pro_rata" # "pro_rata" | "none" exit_notice_days: int = 7 class ConstitutionParams(_Section): version: str = "0.3.0" voting: VotingParams = Field(default_factory=VotingParams) membership: MembershipParams = Field(default_factory=MembershipParams) treasury: TreasuryParams = Field(default_factory=TreasuryParams) emergency: EmergencyParams = Field(default_factory=EmergencyParams) amendments: AmendmentParams = Field(default_factory=AmendmentParams) review: ReviewParams = Field(default_factory=ReviewParams) fork: ForkParams = Field(default_factory=ForkParams) policy: dict[str, Any] = Field(default_factory=dict) # ----- dot-path access ------------------------------------------------- def get_path(self, path: str) -> Any: obj: Any = self for part in path.split("."): if isinstance(obj, dict): if part not in obj: raise KeyError(path) obj = obj[part] else: if not isinstance(obj, BaseModel) or part not in type(obj).model_fields: raise KeyError(path) obj = getattr(obj, part) return obj def set_path(self, path: str, value: Any) -> Any: """Set a dot-path parameter. Returns the previous value. Raises KeyError for paths that do not exist, except under ``policy.`` where new userland keys may be created. """ parts = path.split(".") obj: Any = self for part in parts[:-1]: if isinstance(obj, dict): if part not in obj: raise KeyError(path) obj = obj[part] else: if not isinstance(obj, BaseModel) or part not in type(obj).model_fields: raise KeyError(path) obj = getattr(obj, part) last = parts[-1] if isinstance(obj, dict): old = obj.get(last) obj[last] = value return old if not isinstance(obj, BaseModel) or last not in type(obj).model_fields: raise KeyError(path) old = getattr(obj, last) object.__setattr__(obj, last, value) return old def has_path(self, path: str) -> bool: try: self.get_path(path) return True except KeyError: return False # ----- classification -------------------------------------------------- def is_kernel_path(self, path: str) -> bool: for kp in self.amendments.kernel_paths: if path == kp or path.startswith(kp + "."): return True return False def is_invariant_locked(self, path: str) -> bool: for lp in self.amendments.invariant_locked_paths: if path == lp or path.startswith(lp + "."): return True return False def load_params(path: str | Path) -> ConstitutionParams: with open(path, "r", encoding="utf-8") as fh: data = yaml.safe_load(fh) if not isinstance(data, dict): raise ValueError(f"{path}: expected a mapping at top level") return ConstitutionParams.model_validate(data)