Skip to content

Supervisor

The supervisor subsystem adds a regime-classification and control-proposal layer over oscillator dynamics. It classifies configured regimes, can generate model-based risk estimates, and proposes bounded corrections for review—it does not establish domain-event prediction or close a control loop on hardware.

Pipeline position

UPDEEngine.step() ──→ phases ──→ compute_order_parameter()
                                   UPDEState (R, ψ, locks)
                        ┌─────────────────┼─────────────────┐
                        ↓                 ↓                 ↓
                 RegimeManager     PetriNetAdapter   PredictiveSupervisor
                        │                 │                 │
                        └─────────┬───────┘                 │
                                  ↓                         ↓
                          SupervisorPolicy.decide()  ←──────┘
                                  ├──→ CausalInterventionEngine
                                  │        (baseline vs intervention rollout)
                        list[ControlAction]
                         ActionProjector.project()
                         ActuationMapper.map_actions()

The supervisor sits between the engine output and the next engine step. It consumes UPDEState and BoundaryState, produces ControlAction instructions that modify K_nm, ζ, Ψ, or ω for the next step.


Regime Manager

Finite state machine for synchronisation regimes with hysteresis, cooldown, and event logging.

Regime enum

Value Meaning R range (default thresholds)
NOMINAL Healthy synchronisation R ≥ 0.6
DEGRADED Partial desynchronisation 0.3 ≤ R < 0.6
CRITICAL Synchronisation failure R < 0.3 or hard violation
RECOVERY Transitioning from CRITICAL CRITICAL → R improving

Safety requirement SR-3

CRITICAL must pass through RECOVERY before reaching NOMINAL. Direct CRITICAL → NOMINAL is forbidden. This prevents premature resumption of normal operation after a synchronisation failure.

Constructor

RegimeManager(
    hysteresis: float = 0.05,      # band around thresholds
    cooldown_steps: int = 10,      # steps before next transition
    event_bus: EventBus | None = None,
    hysteresis_hold_steps: int = 0,  # consecutive proposals needed
)

Methods

Method Signature Description
evaluate (UPDEState, BoundaryState) → Regime Proposes regime from metrics
transition (Regime) → Regime Applies FSM rules, returns actual
force_transition (Regime) → Regime Bypasses cooldown

Hysteresis

To prevent oscillation between regimes when R is near a threshold, the manager applies a hysteresis band:

NOMINAL → DEGRADED: requires R < threshold - hysteresis
DEGRADED → NOMINAL: requires R > threshold + hysteresis

hysteresis_hold_steps adds an additional guard: the proposed regime must be proposed for N consecutive steps before the transition fires. CRITICAL always bypasses this hold (safety override).

Cooldown

After a transition, subsequent non-CRITICAL transitions are blocked for cooldown_steps evaluations. CRITICAL always bypasses cooldown.

Transition history

transition_history: deque[tuple[int, Regime, Regime]] stores the last 100 transitions as (step_number, old_regime, new_regime).

Performance: evaluate() < 10 μs.

regimes

Regime classification with hysteresis, cooldown, and optional event emission.

RegimeManager classifies reduced UPDE and boundary state into nominal, degraded, critical, or recovery regimes, then applies cooldown and hysteresis rules before committing transitions. Transition history is bounded and optional events are posted through an injected in-process bus. The manager emits regime state only; policy modules decide any control proposals.

Classes

Regime

Bases: Enum

Operational regime of the SCPN supervisor.

RegimeManager

RegimeManager(
    hysteresis: float = 0.05,
    cooldown_steps: int = 10,
    event_bus: EventBus | None = None,
    hysteresis_hold_steps: int = 0,
)

Classify system state into regimes with hysteresis and cooldown.

Source code in src/scpn_phase_orchestrator/supervisor/regimes.py
def __init__(
    self,
    hysteresis: float = 0.05,
    cooldown_steps: int = 10,
    event_bus: EventBus | None = None,
    hysteresis_hold_steps: int = 0,
) -> None:
    self._hysteresis = _validate_nonnegative_float(
        hysteresis,
        name="hysteresis",
    )
    self._cooldown_steps = _validate_nonnegative_int(
        cooldown_steps,
        name="cooldown_steps",
    )
    if event_bus is not None and not isinstance(event_bus, EventBus):
        raise ValueError(f"event_bus must be an EventBus, got {event_bus!r}")
    self._current = Regime.NOMINAL
    self._step_counter = 0
    self._last_transition_step = -self._cooldown_steps
    self._event_bus = event_bus
    self._hysteresis_hold_steps = _validate_nonnegative_int(
        hysteresis_hold_steps,
        name="hysteresis_hold_steps",
    )
    self._downward_streak = 0
    self.transition_history: deque[tuple[int, Regime, Regime]] = deque(maxlen=100)
Attributes
current_regime property
current_regime: Regime

The regime established after the most recent transition.

Returns

Regime The regime established after the most recent transition.

Methods:
evaluate
evaluate(
    upde_state: UPDEState, boundary_state: BoundaryState
) -> Regime

Propose a regime based on current R values and boundary state.

Parameters

upde_state : UPDEState The current UPDE state. boundary_state : BoundaryState The current boundary-observer state.

Returns

Regime The regime proposed for the current state.

Source code in src/scpn_phase_orchestrator/supervisor/regimes.py
def evaluate(self, upde_state: UPDEState, boundary_state: BoundaryState) -> Regime:
    """Propose a regime based on current R values and boundary state.

    Parameters
    ----------
    upde_state : UPDEState
        The current UPDE state.
    boundary_state : BoundaryState
        The current boundary-observer state.

    Returns
    -------
    Regime
        The regime proposed for the current state.
    """
    if boundary_state.hard_violations:
        return Regime.CRITICAL

    avg_r = self._mean_r(upde_state)

    if avg_r < _R_CRITICAL:
        return Regime.CRITICAL

    is_recovering = self._current in (Regime.CRITICAL, Regime.RECOVERY)

    if avg_r < _R_DEGRADED:
        if is_recovering:
            return Regime.RECOVERY
        return Regime.DEGRADED

    if self._current == Regime.DEGRADED and avg_r < _R_DEGRADED + self._hysteresis:
        return Regime.DEGRADED
    if is_recovering and avg_r < _R_DEGRADED + self._hysteresis:
        return Regime.RECOVERY

    if self._current == Regime.CRITICAL:
        return Regime.RECOVERY

    return Regime.NOMINAL
transition
transition(proposed: Regime) -> Regime

Apply cooldown/hysteresis logic and commit the regime transition.

Parameters

proposed : Regime The proposed regime to transition into.

Returns

Regime The committed regime after cooldown/hysteresis.

Source code in src/scpn_phase_orchestrator/supervisor/regimes.py
def transition(self, proposed: Regime) -> Regime:
    """Apply cooldown/hysteresis logic and commit the regime transition.

    Parameters
    ----------
    proposed : Regime
        The proposed regime to transition into.

    Returns
    -------
    Regime
        The committed regime after cooldown/hysteresis.
    """
    proposed = _validate_regime(proposed)
    self._step_counter += 1

    if proposed == self._current:
        self._downward_streak = 0
        return self._current

    # Soft downward transitions (non-critical) require N consecutive steps
    is_downward = self._regime_rank(proposed) > self._regime_rank(self._current)
    if (
        is_downward
        and proposed != Regime.CRITICAL
        and self._hysteresis_hold_steps > 0
    ):
        self._downward_streak += 1
        if self._downward_streak < self._hysteresis_hold_steps:
            return self._current
    else:
        self._downward_streak = 0

    in_cooldown = (
        self._step_counter - self._last_transition_step
    ) < self._cooldown_steps
    if in_cooldown and proposed != Regime.CRITICAL:
        return self._current

    prev = self._current
    self._last_transition_step = self._step_counter
    self._current = proposed
    self._downward_streak = 0
    self.transition_history.append((self._step_counter, prev, proposed))
    self._emit_transition(prev, proposed)
    return proposed
force_transition
force_transition(regime: Regime) -> Regime

Bypass cooldown and hysteresis hold.

Parameters

regime : Regime The current control regime.

Returns

Regime The regime after a forced transition.

Source code in src/scpn_phase_orchestrator/supervisor/regimes.py
def force_transition(self, regime: Regime) -> Regime:
    """Bypass cooldown and hysteresis hold.

    Parameters
    ----------
    regime : Regime
        The current control regime.

    Returns
    -------
    Regime
        The regime after a forced transition.
    """
    regime = _validate_regime(regime)
    self._step_counter += 1
    prev = self._current
    if regime == prev:
        return prev
    self._last_transition_step = self._step_counter
    self._current = regime
    self._downward_streak = 0
    self.transition_history.append((self._step_counter, prev, regime))
    self._emit_transition(prev, regime)
    return regime

Higher-Order Topology Adaptation

HigherOrderTopologySupervisor is the first supervisor-side topology editor. It consumes live phases plus the current pairwise K_nm matrix and returns a next-step topology:

  • bounded pairwise coupling updates from local phase alignment
  • optional triadic Hyperedge proposals when global coherence is below target
  • pruning of stale or incoherent higher-order edges
  • serialisable audit metadata for added/pruned simplices and pairwise delta norm

The core control knob is TopologyMutationPolicy.mutation_rate. A value of 0.0 freezes topology; larger values increase the maximum per-step pairwise and triadic changes while preserving non-negative couplings and a zero diagonal. TopologyMutationPolicy.simplex_pairwise_support_floor is the policy-hardening gate for deployment reviews: a candidate 2-simplex is only created when every pairwise edge inside that triad is already at or above the configured support floor.

import numpy as np

from scpn_phase_orchestrator.supervisor import (
    HigherOrderTopologySupervisor,
    TopologyMutationPolicy,
)
from scpn_phase_orchestrator.upde.hypergraph import HypergraphEngine

policy = TopologyMutationPolicy(mutation_rate=0.2, coherence_floor=0.8)
topology = HigherOrderTopologySupervisor(policy)
result = topology.mutate(phases, knm)

engine = HypergraphEngine(len(phases), dt=0.01, hyperedges=list(result.hyperedges))
next_phases = engine.step(phases, omegas, pairwise_knm=result.knm)
audit_payload = result.to_audit_record()

This slice does not claim autonomous online structural control. It provides the auditable mutation primitive that existing policy, causal, STL, simplicial, and hypergraph paths can gate before applying a topology change.

Domainpack demos:

  • domainpacks/plasma_control/topology_adaptation_demo.py runs one guarded mutation against the plasma-control binding and prints the audit payload as JSON.
  • domainpacks/traffic_flow/topology_adaptation_demo.py builds pairwise support from transfer-entropy evidence before proposing traffic-corridor simplices, then records Lyapunov before/after energy and basin evidence for the proposed mutation.
  • domainpacks/network_security/topology_adaptation_demo.py builds pairwise support from transfer-entropy evidence before proposing traffic/attack/defence simplices, then records Lyapunov before/after energy evidence for the proposed mutation.

topology

Supervisor-side higher-order topology mutation utilities.

The functions here do not replace the UPDE, simplicial, or hypergraph engines. They prepare the next-step coupling topology from live phase evidence so an existing engine can consume pairwise K_nm and optional triadic hyperedges.

Classes

TopologyMutationPolicy dataclass

TopologyMutationPolicy(
    mutation_rate: float = 0.1,
    coherence_floor: float = 0.75,
    pairwise_threshold: float = 0.85,
    simplex_threshold: float = 0.9,
    max_pairwise_delta: float = 0.05,
    max_simplex_strength: float = 0.2,
    max_new_simplices: int = 4,
    prune_threshold: float = 0.2,
    simplex_pairwise_support_floor: float = 0.0,
    max_coupling: float = 10.0,
)

Policy knobs for one topology mutation step.

mutation_rate is the main supervisor knob: zero freezes topology; one applies the maximum allowed per-step pairwise and triadic changes.

TopologyMutationResult dataclass

TopologyMutationResult(
    knm: FloatArray,
    hyperedges: tuple[Hyperedge, ...],
    added_simplices: tuple[Hyperedge, ...],
    pruned_simplices: tuple[Hyperedge, ...],
    pairwise_delta_norm: float,
    global_coherence: float,
)

Result of a supervisor topology mutation step.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable audit payload for topology mutation.

Returns

dict[str, object] Return a serialisable audit payload for topology mutation.

Source code in src/scpn_phase_orchestrator/supervisor/topology.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable audit payload for topology mutation.

    Returns
    -------
    dict[str, object]
        Return a serialisable audit payload for topology mutation.
    """
    return {
        "global_coherence": self.global_coherence,
        "pairwise_delta_norm": self.pairwise_delta_norm,
        "hyperedge_count": len(self.hyperedges),
        "added_simplices": [
            {"nodes": edge.nodes, "strength": edge.strength}
            for edge in self.added_simplices
        ],
        "pruned_simplices": [
            {"nodes": edge.nodes, "strength": edge.strength}
            for edge in self.pruned_simplices
        ],
    }

HigherOrderTopologySupervisor

HigherOrderTopologySupervisor(
    policy: TopologyMutationPolicy | None = None,
)

Edit pairwise and triadic topology from live phase evidence.

Source code in src/scpn_phase_orchestrator/supervisor/topology.py
def __init__(self, policy: TopologyMutationPolicy | None = None) -> None:
    self.policy = policy or TopologyMutationPolicy()
Methods:
mutate
mutate(
    phases: FloatArray,
    knm: FloatArray,
    hyperedges: tuple[Hyperedge, ...] | None = None,
) -> TopologyMutationResult

Return a mutated topology for the next supervisor actuation step.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). knm : FloatArray Coupling matrix K_nm, shape (N, N). hyperedges : tuple[Hyperedge, ...] | None Existing hyperedges, or None.

Returns

TopologyMutationResult The mutated topology for the next actuation step.

Source code in src/scpn_phase_orchestrator/supervisor/topology.py
def mutate(
    self,
    phases: FloatArray,
    knm: FloatArray,
    hyperedges: tuple[Hyperedge, ...] | None = None,
) -> TopologyMutationResult:
    """Return a mutated topology for the next supervisor actuation step.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    hyperedges : tuple[Hyperedge, ...] | None
        Existing hyperedges, or ``None``.

    Returns
    -------
    TopologyMutationResult
        The mutated topology for the next actuation step.
    """
    phases_arr = _validate_phases(phases)
    knm_arr = _validate_knm(knm, phases_arr.size)
    existing = _canonical_hyperedges(tuple(hyperedges or ()))
    _validate_hyperedges(existing, phases_arr.size)

    global_coherence = _order_parameter(phases_arr)
    if self.policy.mutation_rate == 0.0:
        return TopologyMutationResult(
            knm=knm_arr.copy(),
            hyperedges=existing,
            added_simplices=(),
            pruned_simplices=(),
            pairwise_delta_norm=0.0,
            global_coherence=global_coherence,
        )

    local = _pairwise_phase_alignment(phases_arr)
    mutated_knm = _mutate_pairwise(knm_arr, local, self.policy)
    kept, pruned = _prune_simplices(existing, phases_arr, self.policy)
    added = _candidate_simplices(
        phases_arr,
        mutated_knm,
        kept,
        self.policy,
        global_coherence,
    )
    hyperedge_map = {edge.nodes: edge for edge in kept}
    for edge in added:
        hyperedge_map[edge.nodes] = edge
    hyperedge_tuple = tuple(hyperedge_map[nodes] for nodes in sorted(hyperedge_map))

    return TopologyMutationResult(
        knm=mutated_knm,
        hyperedges=hyperedge_tuple,
        added_simplices=added,
        pruned_simplices=pruned,
        pairwise_delta_norm=float(np.linalg.norm(mutated_knm - knm_arr)),
        global_coherence=global_coherence,
    )

Functions:


Hierarchical Orchestration Summaries

build_hierarchical_orchestration_plan() is the generic nested-supervisor foundation. Child supervisors exchange bounded summaries only: child name, channel, R, psi, regime, confidence, and optional metadata. The parent planner converts those summaries into a reduced UPDEState, computes cross-child phase alignment, and emits escalation records for low confidence, degraded coherence, critical coherence, or explicit child-regime escalation.

from scpn_phase_orchestrator.supervisor import (
    ChildSupervisorSummary,
    build_hierarchical_orchestration_plan,
)

plan = build_hierarchical_orchestration_plan(
    [
        ChildSupervisorSummary("edge-a", "power", R=0.9, psi=0.0),
        ChildSupervisorSummary("edge-b", "thermal", R=0.5, psi=1.2),
    ],
    degraded_threshold=0.65,
    critical_threshold=0.35,
)

parent_state = plan.parent_state
audit_payload = plan.to_audit_record()

The same reduced summaries can be wrapped in deterministic sync envelopes for JSONL replay, message-bus transport, or parent-side cloud ingestion. The parent ingestion helper rejects stale or duplicate sequence numbers per source node and protocol-version mismatches before building the parent orchestration plan. Direct envelope JSON parsing uses canonical finite JSON semantics: non-finite constants and duplicate object keys are rejected before the reduced summary is validated or admitted to the parent watermark ledger.

from scpn_phase_orchestrator.supervisor import (
    build_hierarchy_sync_envelope,
    ingest_hierarchy_sync_envelopes,
)

envelope = build_hierarchy_sync_envelope(
    ChildSupervisorSummary("edge-a", "power", R=0.9, psi=0.0),
    source_node="edge-node-a",
    sequence=42,
)

ledger = ingest_hierarchy_sync_envelopes(
    [envelope],
    previous_sequences={"edge-node-a": 41},
)
sync_audit = ledger.to_audit_record()

HierarchyTransportRuntime is the next live-transport boundary. Caller-owned REST, gRPC, Kafka, file, or hardware adapters can pass decoded mappings or JSON strings into the runtime; the runtime parses reduced sync records, maintains per-source sequence watermarks across batches, and emits the same parent ledger. It still owns no socket, thread, broker client, or actuator handle.

from scpn_phase_orchestrator.supervisor import HierarchyTransportRuntime

runtime = HierarchyTransportRuntime()
batch_ledger = runtime.ingest_batch([envelope.to_json()])
runtime_audit = runtime.to_audit_record()

For offline distributed-edge testing, simulate_hierarchy_gossip_consensus() replays local consensus over accepted sync envelopes and a caller-supplied neighbour map. Each node updates only its reduced coherence, phase, confidence, and audit metadata; no sockets are opened and no raw observations enter the consensus state.

from scpn_phase_orchestrator.supervisor import simulate_hierarchy_gossip_consensus

rounds = simulate_hierarchy_gossip_consensus(
    [envelope],
    neighbour_map={"edge-node-a": ()},
    rounds=1,
)
consensus_audit = [round_record.to_audit_record() for round_record in rounds]

This slice does not open sockets, run a gossip protocol, or perform direct actuation. It gives existing regime, policy, FEP, causal, STL, and audit paths a common parent-level state built from reduced child evidence without moving raw time series, local coupling matrices, or actuator targets across hierarchy boundaries.

Domainpack demos:

  • domainpacks/power_grid/hierarchy_sync_demo.py replays generation and demand/renewable edge summaries through the sync-envelope ingestion path.
  • domainpacks/cardiac_rhythm/hierarchy_sync_demo.py replays pacemaker/atrial and ventricular/recovery summaries through the same parent planner.

hierarchy

Reduced-evidence hierarchy summaries, envelopes, ledgers, and consensus.

The hierarchy package enforces a boundary where parent supervisors receive only bounded child summaries: coherence, phase, regime, confidence, channel, and metadata. Raw phases, time series, coupling matrices, event payloads, and actuator targets are rejected from metadata and transport envelopes. The boundary types and validation live in one core module, with the orchestration plan, sync transport, and gossip consensus split into their own modules behind a stable re-export surface. Builders and runtimes are socket-free and return audit-ready plans or ledgers.

Classes

ChildSupervisorSummary dataclass

ChildSupervisorSummary(
    name: str,
    channel: str,
    R: float,
    psi: float,
    regime: str = _REGIME_NOMINAL,
    confidence: float = 1.0,
    metadata: Mapping[str, object] = dict(),
)

Bounded child-supervisor evidence for parent orchestration.

The summary intentionally carries reduced coherence evidence only. Raw child phases, time series, local coupling matrices, and actuator targets do not cross the hierarchy boundary in this foundation slice.

Attributes
weighted_R property
weighted_R: float

Return coherence weighted by summary confidence.

Returns

float Return coherence weighted by summary confidence.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe reduced child summary.

Returns

dict[str, object] Return a JSON-safe reduced child summary.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/boundary.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe reduced child summary.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe reduced child summary.
    """
    return {
        "name": self.name,
        "channel": self.channel,
        "R": float(self.R),
        "psi": float(self.psi),
        "regime": self.regime,
        "confidence": float(self.confidence),
        "weighted_R": self.weighted_R,
        "metadata": _metadata_to_audit_record(self.metadata),
    }

HierarchyEscalation dataclass

HierarchyEscalation(
    child: str,
    channel: str,
    severity: str,
    reason: str,
    R: float,
    confidence: float,
    child_regime: str,
)

Bounded evidence escalated from a child to the parent supervisor.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe escalation record.

Returns

dict[str, object] Return a JSON-safe escalation record.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/boundary.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe escalation record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe escalation record.
    """
    return {
        "child": self.child,
        "channel": self.channel,
        "severity": self.severity,
        "reason": self.reason,
        "R": float(self.R),
        "confidence": float(self.confidence),
        "child_regime": self.child_regime,
    }

HierarchySyncEnvelope dataclass

HierarchySyncEnvelope(
    protocol_version: str,
    source_node: str,
    sequence: int,
    summary: ChildSupervisorSummary,
    monotonic_time_s: float | None = None,
)

Transport-neutral hierarchy summary exchanged by edge/cloud nodes.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe transport envelope audit record.

Returns

dict[str, object] Return a JSON-safe transport envelope audit record.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/boundary.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe transport envelope audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe transport envelope audit record.
    """
    record: dict[str, object] = {
        "protocol_version": self.protocol_version,
        "source_node": self.source_node,
        "sequence": self.sequence,
        "summary": self.summary.to_audit_record(),
    }
    if self.monotonic_time_s is not None:
        record["monotonic_time_s"] = float(self.monotonic_time_s)
    return record
to_json
to_json() -> str

Serialise the envelope with deterministic key ordering.

Returns

str Serialise the envelope with deterministic key ordering.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/boundary.py
def to_json(self) -> str:
    """Serialise the envelope with deterministic key ordering.

    Returns
    -------
    str
        Serialise the envelope with deterministic key ordering.
    """
    return json.dumps(self.to_audit_record(), sort_keys=True, separators=(",", ":"))

HierarchyConsensusRound dataclass

HierarchyConsensusRound(
    round_index: int,
    states: tuple[HierarchyConsensusState, ...],
    plan: HierarchicalOrchestrationPlan,
    rejected: tuple[dict[str, object], ...] = (),
)

Deterministic non-networked gossip/local-consensus replay result.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe consensus-round audit record.

Returns

dict[str, object] Return a JSON-safe consensus-round audit record.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/consensus.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe consensus-round audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe consensus-round audit record.
    """
    return {
        "round_index": self.round_index,
        "states": [state.to_audit_record() for state in self.states],
        "rejected": list(self.rejected),
        "plan": self.plan.to_audit_record(),
    }

HierarchyConsensusState dataclass

HierarchyConsensusState(
    source_node: str,
    sequence: int,
    summary: ChildSupervisorSummary,
)

Reduced node state after an offline hierarchy gossip round.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe consensus node record.

Returns

dict[str, object] Return a JSON-safe consensus node record.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/consensus.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe consensus node record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe consensus node record.
    """
    return {
        "source_node": self.source_node,
        "sequence": self.sequence,
        "summary": self.summary.to_audit_record(),
    }

HierarchicalOrchestrationPlan dataclass

HierarchicalOrchestrationPlan(
    hierarchy: str,
    children: tuple[ChildSupervisorSummary, ...],
    parent_state: UPDEState,
    escalations: tuple[HierarchyEscalation, ...],
    parent_R: float,
    parent_psi: float,
    audit_scope: str = _AUDIT_SCOPE_REDUCED_SUMMARIES,
)

Parent orchestration input built from reduced child summaries.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable plan record for hierarchy audit logs.

Returns

dict[str, object] Return a serialisable plan record for hierarchy audit logs.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/plan.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable plan record for hierarchy audit logs.

    Returns
    -------
    dict[str, object]
        Return a serialisable plan record for hierarchy audit logs.
    """
    return {
        "hierarchy": self.hierarchy,
        "audit_scope": self.audit_scope,
        "parent": {
            "R": float(self.parent_R),
            "psi": float(self.parent_psi),
            "stability_proxy": float(self.parent_state.stability_proxy),
            "regime_id": self.parent_state.regime_id,
            "layer_count": len(self.parent_state.layers),
        },
        "children": [child.to_audit_record() for child in self.children],
        "escalations": [
            escalation.to_audit_record() for escalation in self.escalations
        ],
    }

HierarchySyncLedger dataclass

HierarchySyncLedger(
    accepted: tuple[HierarchySyncEnvelope, ...],
    rejected: tuple[dict[str, object], ...],
    plan: HierarchicalOrchestrationPlan,
)

Parent-side ingestion result for sync envelopes.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable sync-ingestion audit payload.

Returns

dict[str, object] Return a serialisable sync-ingestion audit payload.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable sync-ingestion audit payload.

    Returns
    -------
    dict[str, object]
        Return a serialisable sync-ingestion audit payload.
    """
    return {
        "accepted": [envelope.to_audit_record() for envelope in self.accepted],
        "rejected": list(self.rejected),
        "plan": self.plan.to_audit_record(),
    }

HierarchyTransportRuntime

HierarchyTransportRuntime(
    *,
    previous_sequences: Mapping[str, int] | None = None,
    hierarchy: str = "edge_cloud_summary_sync",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
)

Socket-free runtime state for hierarchy transport adapters.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def __init__(
    self,
    *,
    previous_sequences: Mapping[str, int] | None = None,
    hierarchy: str = "edge_cloud_summary_sync",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
) -> None:
    _require_non_empty(hierarchy, "hierarchy")
    _require_non_empty(protocol_version, "protocol_version")
    _require_unit_interval(degraded_threshold, "degraded_threshold")
    _require_unit_interval(critical_threshold, "critical_threshold")
    _require_unit_interval(min_confidence, "min_confidence")
    if critical_threshold > degraded_threshold:
        raise ValueError("critical_threshold must be <= degraded_threshold")
    self._previous_sequences = _normalise_previous_sequences(previous_sequences)
    self._hierarchy = hierarchy
    self._degraded_threshold = degraded_threshold
    self._critical_threshold = critical_threshold
    self._min_confidence = min_confidence
    self._protocol_version = protocol_version
Attributes
previous_sequences property
previous_sequences: dict[str, int]

Return the accepted per-source sequence watermarks.

Returns

dict[str, int] Return the accepted per-source sequence watermarks.

Methods:
ingest
ingest(
    records: Sequence[
        HierarchySyncEnvelope | Mapping[str, object] | str
    ],
) -> HierarchySyncLedger

Parse a transport batch, ingest it, and advance accepted watermarks.

Parameters

records : Sequence[HierarchySyncEnvelope | Mapping[str, object] | str] The transport records to ingest.

Returns

HierarchySyncLedger The sync ledger with advanced watermarks.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def ingest(
    self,
    records: Sequence[HierarchySyncEnvelope | Mapping[str, object] | str],
) -> HierarchySyncLedger:
    """Parse a transport batch, ingest it, and advance accepted watermarks.

    Parameters
    ----------
    records : Sequence[HierarchySyncEnvelope | Mapping[str, object] | str]
        The transport records to ingest.

    Returns
    -------
    HierarchySyncLedger
        The sync ledger with advanced watermarks.
    """
    envelopes = tuple(load_hierarchy_sync_envelope(record) for record in records)
    ledger = ingest_hierarchy_sync_envelopes(
        envelopes,
        previous_sequences=self._previous_sequences,
        hierarchy=self._hierarchy,
        degraded_threshold=self._degraded_threshold,
        critical_threshold=self._critical_threshold,
        min_confidence=self._min_confidence,
        protocol_version=self._protocol_version,
    )
    for envelope in ledger.accepted:
        self._previous_sequences[envelope.source_node] = envelope.sequence
    return ledger
ingest_batch
ingest_batch(
    records: Sequence[
        HierarchySyncEnvelope | Mapping[str, object] | str
    ],
) -> HierarchySyncLedger

Alias for adapter batch ingestion.

Parameters

records : Sequence[HierarchySyncEnvelope | Mapping[str, object] | str] The transport records to ingest.

Returns

HierarchySyncLedger The sync ledger for the ingested batch.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def ingest_batch(
    self,
    records: Sequence[HierarchySyncEnvelope | Mapping[str, object] | str],
) -> HierarchySyncLedger:
    """Alias for adapter batch ingestion.

    Parameters
    ----------
    records : Sequence[HierarchySyncEnvelope | Mapping[str, object] | str]
        The transport records to ingest.

    Returns
    -------
    HierarchySyncLedger
        The sync ledger for the ingested batch.
    """
    return self.ingest(records)
to_audit_record
to_audit_record() -> dict[str, object]

Return socket-free runtime state for audit logging.

Returns

dict[str, object] Return socket-free runtime state for audit logging.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def to_audit_record(self) -> dict[str, object]:
    """Return socket-free runtime state for audit logging.

    Returns
    -------
    dict[str, object]
        Return socket-free runtime state for audit logging.
    """
    return {
        "hierarchy": self._hierarchy,
        "protocol_version": self._protocol_version,
        "previous_sequences": {
            source: self._previous_sequences[source]
            for source in sorted(self._previous_sequences)
        },
        "audit_scope": _AUDIT_SCOPE_REDUCED_SUMMARIES,
    }

Functions:

simulate_hierarchy_gossip_consensus

simulate_hierarchy_gossip_consensus(
    envelopes: Sequence[HierarchySyncEnvelope],
    *,
    neighbour_map: Mapping[str, Sequence[str]],
    rounds: int = 1,
    self_weight: float = 0.5,
    hierarchy: str = "offline_hierarchy_gossip_consensus",
    previous_sequences: Mapping[str, int] | None = None,
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
) -> tuple[HierarchyConsensusRound, ...]

Replay local consensus over hierarchy sync envelopes without networking.

Each round updates every accepted node from its own reduced summary and the summaries of configured neighbours. The update averages confidence-weighted coherence and circular phase only; raw child observations never enter the consensus state. This is a deterministic simulation surface for testing distributed orchestration policies before any live gossip transport exists.

Parameters

envelopes : Sequence[HierarchySyncEnvelope] The ordered transport envelopes. neighbour_map : Mapping[str, Sequence[str]] Per-node neighbour lists for gossip. rounds : int Number of gossip rounds. self_weight : float Self-weight in the gossip consensus update. hierarchy : str Hierarchy label. previous_sequences : Mapping[str, int] | None Accepted per-source sequence watermarks, or None. degraded_threshold : float Coherence threshold below which a child is degraded. critical_threshold : float Coherence threshold below which a child is critical. min_confidence : float Minimum child summary confidence to include. protocol_version : str Hierarchy sync protocol version.

Returns

tuple[HierarchyConsensusRound, ...] The per-round gossip consensus states.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/consensus.py
def simulate_hierarchy_gossip_consensus(
    envelopes: Sequence[HierarchySyncEnvelope],
    *,
    neighbour_map: Mapping[str, Sequence[str]],
    rounds: int = 1,
    self_weight: float = 0.5,
    hierarchy: str = "offline_hierarchy_gossip_consensus",
    previous_sequences: Mapping[str, int] | None = None,
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
) -> tuple[HierarchyConsensusRound, ...]:
    """Replay local consensus over hierarchy sync envelopes without networking.

    Each round updates every accepted node from its own reduced summary and the
    summaries of configured neighbours. The update averages confidence-weighted
    coherence and circular phase only; raw child observations never enter the
    consensus state. This is a deterministic simulation surface for testing
    distributed orchestration policies before any live gossip transport exists.

    Parameters
    ----------
    envelopes : Sequence[HierarchySyncEnvelope]
        The ordered transport envelopes.
    neighbour_map : Mapping[str, Sequence[str]]
        Per-node neighbour lists for gossip.
    rounds : int
        Number of gossip rounds.
    self_weight : float
        Self-weight in the gossip consensus update.
    hierarchy : str
        Hierarchy label.
    previous_sequences : Mapping[str, int] | None
        Accepted per-source sequence watermarks, or ``None``.
    degraded_threshold : float
        Coherence threshold below which a child is degraded.
    critical_threshold : float
        Coherence threshold below which a child is critical.
    min_confidence : float
        Minimum child summary confidence to include.
    protocol_version : str
        Hierarchy sync protocol version.

    Returns
    -------
    tuple[HierarchyConsensusRound, ...]
        The per-round gossip consensus states.
    """
    _validate_gossip_inputs(rounds=rounds, self_weight=self_weight)
    _validate_neighbour_map(neighbour_map)
    ledger = ingest_hierarchy_sync_envelopes(
        envelopes,
        previous_sequences=previous_sequences,
        hierarchy=hierarchy,
        degraded_threshold=degraded_threshold,
        critical_threshold=critical_threshold,
        min_confidence=min_confidence,
        protocol_version=protocol_version,
    )
    current = {
        envelope.source_node: HierarchyConsensusState(
            source_node=envelope.source_node,
            sequence=envelope.sequence,
            summary=envelope.summary,
        )
        for envelope in ledger.accepted
    }
    history: list[HierarchyConsensusRound] = []
    for round_index in range(1, rounds + 1):
        current = _advance_consensus_round(
            current,
            neighbour_map=neighbour_map,
            self_weight=self_weight,
            degraded_threshold=degraded_threshold,
            critical_threshold=critical_threshold,
        )
        states = tuple(current[node] for node in sorted(current))
        plan = build_hierarchical_orchestration_plan(
            [state.summary for state in states],
            hierarchy=f"{hierarchy}_round_{round_index}",
            degraded_threshold=degraded_threshold,
            critical_threshold=critical_threshold,
            min_confidence=min_confidence,
        )
        history.append(
            HierarchyConsensusRound(
                round_index=round_index,
                states=states,
                plan=plan,
                rejected=ledger.rejected if round_index == 1 else (),
            )
        )
    return tuple(history)

build_hierarchical_orchestration_plan

build_hierarchical_orchestration_plan(
    children: Iterable[ChildSupervisorSummary],
    *,
    hierarchy: str = "child_supervisors_to_parent",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
) -> HierarchicalOrchestrationPlan

Build a parent UPDE state and escalation set from child summaries.

This is a non-networked hierarchy foundation. It composes child coherence summaries into a parent-level UPDEState so existing regime, policy, FEP, causal, and audit paths can reason over nested supervisors without reading raw child observations.

Parameters

children : Iterable[ChildSupervisorSummary] Child supervisor summaries. hierarchy : str Hierarchy label. degraded_threshold : float Coherence threshold below which a child is degraded. critical_threshold : float Coherence threshold below which a child is critical. min_confidence : float Minimum child summary confidence to include.

Returns

HierarchicalOrchestrationPlan The parent plan and escalation set.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/plan.py
def build_hierarchical_orchestration_plan(
    children: Iterable[ChildSupervisorSummary],
    *,
    hierarchy: str = "child_supervisors_to_parent",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
) -> HierarchicalOrchestrationPlan:
    """Build a parent UPDE state and escalation set from child summaries.

    This is a non-networked hierarchy foundation. It composes child coherence
    summaries into a parent-level ``UPDEState`` so existing regime, policy, FEP,
    causal, and audit paths can reason over nested supervisors without reading
    raw child observations.

    Parameters
    ----------
    children : Iterable[ChildSupervisorSummary]
        Child supervisor summaries.
    hierarchy : str
        Hierarchy label.
    degraded_threshold : float
        Coherence threshold below which a child is degraded.
    critical_threshold : float
        Coherence threshold below which a child is critical.
    min_confidence : float
        Minimum child summary confidence to include.

    Returns
    -------
    HierarchicalOrchestrationPlan
        The parent plan and escalation set.
    """
    child_tuple = tuple(children)
    _validate_plan_inputs(
        children=child_tuple,
        hierarchy=hierarchy,
        degraded_threshold=degraded_threshold,
        critical_threshold=critical_threshold,
        min_confidence=min_confidence,
    )

    weighted_r = np.asarray(
        [child.weighted_R for child in child_tuple],
        dtype=np.float64,
    )
    phases = np.asarray([child.psi for child in child_tuple], dtype=np.float64)
    parent_r, parent_psi = _weighted_order_parameter(weighted_r, phases)
    parent_regime = _parent_regime(
        parent_r,
        degraded_threshold=degraded_threshold,
        critical_threshold=critical_threshold,
    )
    parent_state = UPDEState(
        layers=[
            LayerState(R=child.weighted_R, psi=float(child.psi))
            for child in child_tuple
        ],
        cross_layer_alignment=_cross_child_alignment(phases),
        stability_proxy=float(np.mean(weighted_r)),
        regime_id=f"hierarchical_{parent_regime}",
    )
    escalations = tuple(
        escalation
        for child in child_tuple
        for escalation in _child_escalations(
            child,
            degraded_threshold=degraded_threshold,
            critical_threshold=critical_threshold,
            min_confidence=min_confidence,
        )
    )
    return HierarchicalOrchestrationPlan(
        hierarchy=hierarchy,
        children=child_tuple,
        parent_state=parent_state,
        escalations=escalations,
        parent_R=parent_r,
        parent_psi=parent_psi,
    )

build_hierarchy_sync_envelope

build_hierarchy_sync_envelope(
    summary: ChildSupervisorSummary,
    *,
    source_node: str,
    sequence: int,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
    monotonic_time_s: float | None = None,
) -> HierarchySyncEnvelope

Build a deterministic edge/cloud hierarchy sync envelope.

The envelope is transport-neutral: callers may write it to JSONL, send it over a message bus, or hand it to tests without this module opening sockets or performing live deployment work.

Parameters

summary : ChildSupervisorSummary The child supervisor summary. source_node : str Identifier of the source node. sequence : int Monotonic envelope sequence number. protocol_version : str Hierarchy sync protocol version. monotonic_time_s : float | None Monotonic timestamp in seconds, or None.

Returns

HierarchySyncEnvelope The deterministic hierarchy sync envelope.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def build_hierarchy_sync_envelope(
    summary: ChildSupervisorSummary,
    *,
    source_node: str,
    sequence: int,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
    monotonic_time_s: float | None = None,
) -> HierarchySyncEnvelope:
    """Build a deterministic edge/cloud hierarchy sync envelope.

    The envelope is transport-neutral: callers may write it to JSONL, send it
    over a message bus, or hand it to tests without this module opening sockets
    or performing live deployment work.

    Parameters
    ----------
    summary : ChildSupervisorSummary
        The child supervisor summary.
    source_node : str
        Identifier of the source node.
    sequence : int
        Monotonic envelope sequence number.
    protocol_version : str
        Hierarchy sync protocol version.
    monotonic_time_s : float | None
        Monotonic timestamp in seconds, or ``None``.

    Returns
    -------
    HierarchySyncEnvelope
        The deterministic hierarchy sync envelope.
    """
    return HierarchySyncEnvelope(
        protocol_version=protocol_version,
        source_node=source_node,
        sequence=sequence,
        summary=summary,
        monotonic_time_s=monotonic_time_s,
    )

ingest_hierarchy_sync_envelopes

ingest_hierarchy_sync_envelopes(
    envelopes: Sequence[HierarchySyncEnvelope],
    *,
    previous_sequences: Mapping[str, int] | None = None,
    hierarchy: str = "edge_cloud_summary_sync",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
) -> HierarchySyncLedger

Validate envelopes and build a parent plan from accepted summaries.

Parent nodes reject stale or duplicate sequence numbers per source node and reject protocol-version mismatches. Accepted envelopes are sorted by source node and sequence before parent-state composition, making JSONL replay and cloud ingestion deterministic.

Parameters

envelopes : Sequence[HierarchySyncEnvelope] The ordered transport envelopes. previous_sequences : Mapping[str, int] | None Accepted per-source sequence watermarks, or None. hierarchy : str Hierarchy label. degraded_threshold : float Coherence threshold below which a child is degraded. critical_threshold : float Coherence threshold below which a child is critical. min_confidence : float Minimum child summary confidence to include. protocol_version : str Hierarchy sync protocol version.

Returns

HierarchySyncLedger The sync ledger built from accepted summaries.

Raises

ValueError If an envelope fails validation.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def ingest_hierarchy_sync_envelopes(
    envelopes: Sequence[HierarchySyncEnvelope],
    *,
    previous_sequences: Mapping[str, int] | None = None,
    hierarchy: str = "edge_cloud_summary_sync",
    degraded_threshold: float = 0.65,
    critical_threshold: float = 0.35,
    min_confidence: float = 0.5,
    protocol_version: str = _DEFAULT_HIERARCHY_SYNC_PROTOCOL,
) -> HierarchySyncLedger:
    """Validate envelopes and build a parent plan from accepted summaries.

    Parent nodes reject stale or duplicate sequence numbers per source node and
    reject protocol-version mismatches. Accepted envelopes are sorted by source
    node and sequence before parent-state composition, making JSONL replay and
    cloud ingestion deterministic.

    Parameters
    ----------
    envelopes : Sequence[HierarchySyncEnvelope]
        The ordered transport envelopes.
    previous_sequences : Mapping[str, int] | None
        Accepted per-source sequence watermarks, or ``None``.
    hierarchy : str
        Hierarchy label.
    degraded_threshold : float
        Coherence threshold below which a child is degraded.
    critical_threshold : float
        Coherence threshold below which a child is critical.
    min_confidence : float
        Minimum child summary confidence to include.
    protocol_version : str
        Hierarchy sync protocol version.

    Returns
    -------
    HierarchySyncLedger
        The sync ledger built from accepted summaries.

    Raises
    ------
    ValueError
        If an envelope fails validation.
    """
    canonical_envelopes = tuple(
        _canonical_hierarchy_sync_envelope(envelope) for envelope in envelopes
    )
    for envelope in canonical_envelopes:
        _validate_envelope_reduced_only(envelope)
    _validate_plan_inputs(
        children=[envelope.summary for envelope in canonical_envelopes]
        or [_dummy_summary()],
        hierarchy=hierarchy,
        degraded_threshold=degraded_threshold,
        critical_threshold=critical_threshold,
        min_confidence=min_confidence,
    )
    expected_sequences = _normalise_previous_sequences(previous_sequences)
    accepted: list[HierarchySyncEnvelope] = []
    rejected: list[dict[str, object]] = []
    valid_protocol: list[HierarchySyncEnvelope] = []
    for envelope in sorted(
        canonical_envelopes,
        key=lambda item: (item.source_node, item.sequence),
    ):
        if envelope.protocol_version != protocol_version:
            rejected.append(_rejection(envelope, "protocol_version_mismatch"))
            continue
        valid_protocol.append(envelope)

    duplicate_sequence_conflict_sources = _duplicate_sequence_conflict_sources(
        valid_protocol
    )
    latest_by_source: dict[str, HierarchySyncEnvelope] = {}
    for envelope in valid_protocol:
        if envelope.source_node in duplicate_sequence_conflict_sources:
            continue
        latest = latest_by_source.get(envelope.source_node)
        if latest is None or envelope.sequence > latest.sequence:
            latest_by_source[envelope.source_node] = envelope

    for envelope in valid_protocol:
        if envelope.source_node in duplicate_sequence_conflict_sources:
            rejected.append(_rejection(envelope, "duplicate_sequence_conflict"))
            continue
        latest = latest_by_source[envelope.source_node]
        previous_sequence = expected_sequences.get(envelope.source_node, -1)
        if envelope.sequence <= previous_sequence or envelope is not latest:
            rejected.append(_rejection(envelope, "stale_or_duplicate_sequence"))
            continue
        expected_sequences[envelope.source_node] = envelope.sequence
        accepted.append(envelope)

    accepted_tuple = tuple(
        sorted(accepted, key=lambda item: (item.source_node, item.sequence))
    )
    if not accepted_tuple:
        raise ValueError("at least one hierarchy sync envelope must be accepted")
    plan = build_hierarchical_orchestration_plan(
        [envelope.summary for envelope in accepted_tuple],
        hierarchy=hierarchy,
        degraded_threshold=degraded_threshold,
        critical_threshold=critical_threshold,
        min_confidence=min_confidence,
    )
    return HierarchySyncLedger(
        accepted=accepted_tuple,
        rejected=tuple(
            sorted(
                rejected,
                key=_rejection_sort_key,
            )
        ),
        plan=plan,
    )

load_hierarchy_sync_envelope

load_hierarchy_sync_envelope(
    record: HierarchySyncEnvelope
    | Mapping[str, object]
    | str,
) -> HierarchySyncEnvelope

Parse a JSON string or decoded mapping into a strict sync envelope.

Parameters

record : HierarchySyncEnvelope | Mapping[str, object] | str A sync envelope, decoded mapping, or JSON string.

Returns

HierarchySyncEnvelope The parsed strict sync envelope.

Raises

ValueError If the record cannot be parsed into a strict envelope.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy/sync.py
def load_hierarchy_sync_envelope(
    record: HierarchySyncEnvelope | Mapping[str, object] | str,
) -> HierarchySyncEnvelope:
    """Parse a JSON string or decoded mapping into a strict sync envelope.

    Parameters
    ----------
    record : HierarchySyncEnvelope | Mapping[str, object] | str
        A sync envelope, decoded mapping, or JSON string.

    Returns
    -------
    HierarchySyncEnvelope
        The parsed strict sync envelope.

    Raises
    ------
    ValueError
        If the record cannot be parsed into a strict envelope.
    """
    if isinstance(record, HierarchySyncEnvelope):
        _validate_envelope_reduced_only(record)
        return _canonical_hierarchy_sync_envelope(record)
    payload = _load_mapping_record(record)
    _reject_raw_hierarchy_keys(payload, "hierarchy sync envelope")
    _reject_unknown_keys(
        payload,
        allowed=_HIERARCHY_SYNC_ENVELOPE_KEYS,
        location="hierarchy sync envelope",
    )
    summary_record = payload.get("summary")
    if not isinstance(summary_record, Mapping):
        raise ValueError("summary must be a decoded mapping")
    _reject_raw_hierarchy_keys(summary_record, "hierarchy sync summary")
    _reject_unknown_keys(
        summary_record,
        allowed=_HIERARCHY_SYNC_SUMMARY_KEYS,
        location="hierarchy sync summary",
    )

    sequence = _require_integer(payload.get("sequence"), "sequence")
    monotonic_time_s = payload.get("monotonic_time_s")
    if monotonic_time_s is not None:
        monotonic_time_s = _require_float(monotonic_time_s, "monotonic_time_s")
        if monotonic_time_s < 0.0:
            raise ValueError("monotonic_time_s must be finite and non-negative")

    envelope = HierarchySyncEnvelope(
        protocol_version=_require_text_field(payload, "protocol_version"),
        source_node=_require_text_field(payload, "source_node"),
        sequence=sequence,
        summary=_load_child_summary(summary_record),
        monotonic_time_s=monotonic_time_s,
    )
    _validate_envelope_reduced_only(envelope)
    return envelope

Hierarchy Adapter Boundaries

hierarchy_adapters adds decoded JSONL, REST-payload, and WebSocket-frame helpers over HierarchyTransportRuntime. These helpers are transport boundaries only: they do not open sockets, own HTTP servers, start event loops, or apply actuation. They return HierarchyAdapterResult records containing accepted/rejected counts, sequence watermarks, parent-plan summaries, and the underlying sync ledger.

hierarchy_adapters

Decoded REST, WebSocket-frame, and JSONL hierarchy adapter boundaries.

The adapter helpers validate already-decoded payloads, content-type headers, frame kinds, and envelope batches before passing records into a socket-free HierarchyTransportRuntime. They return audit-safe result records with watermarks and ledgers. The module deliberately owns no HTTP server, WebSocket, filesystem tailer, or retry loop.

Classes

HierarchyAdapterResult dataclass

HierarchyAdapterResult(
    boundary: str,
    ledger: HierarchySyncLedger,
    watermarks: Mapping[str, int],
    frame_kind: str | None = None,
    status: str = "accepted",
)

Audit-safe result returned by decoded hierarchy adapter boundaries.

Attributes
accepted_count property
accepted_count: int

Return the number of envelopes accepted by the runtime.

Returns

int Return the number of envelopes accepted by the runtime.

rejected_count property
rejected_count: int

Return the number of envelopes rejected by the runtime.

Returns

int Return the number of envelopes rejected by the runtime.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe adapter audit payload.

Returns

dict[str, object] Return a deterministic JSON-safe adapter audit payload.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy_adapters.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe adapter audit payload.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe adapter audit payload.
    """
    plan = self.ledger.plan
    record: dict[str, object] = {
        "boundary": self.boundary,
        "status": self.status,
        "accepted_count": self.accepted_count,
        "rejected_count": self.rejected_count,
        "watermarks": {
            source: int(self.watermarks[source])
            for source in sorted(self.watermarks)
        },
        "parent_plan": {
            "R": float(plan.parent_R),
            "psi": float(plan.parent_psi),
            "regime_id": plan.parent_state.regime_id,
            "layer_count": len(plan.parent_state.layers),
        },
        "ledger": self.ledger.to_audit_record(),
    }
    if self.frame_kind is not None:
        record["frame_kind"] = self.frame_kind
    return record

Functions:

replay_hierarchy_jsonl

replay_hierarchy_jsonl(
    lines: Iterable[
        str | Mapping[str, object] | HierarchySyncEnvelope
    ],
    *,
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult

Replay decoded or JSONL hierarchy records through a socket-free runtime.

Parameters

lines : Iterable[str | Mapping[str, object] | HierarchySyncEnvelope] Decoded or JSONL hierarchy records. runtime : HierarchyTransportRuntime | None The socket-free transport runtime, or None.

Returns

HierarchyAdapterResult The adapter result for the replayed records.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy_adapters.py
def replay_hierarchy_jsonl(
    lines: Iterable[str | Mapping[str, object] | HierarchySyncEnvelope],
    *,
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult:
    """Replay decoded or JSONL hierarchy records through a socket-free runtime.

    Parameters
    ----------
    lines : Iterable[str | Mapping[str, object] | HierarchySyncEnvelope]
        Decoded or JSONL hierarchy records.
    runtime : HierarchyTransportRuntime | None
        The socket-free transport runtime, or ``None``.

    Returns
    -------
    HierarchyAdapterResult
        The adapter result for the replayed records.
    """
    active_runtime = runtime or HierarchyTransportRuntime()
    records = tuple(
        _load_jsonl_record(line, index) for index, line in enumerate(lines, 1)
    )
    ledger = active_runtime.ingest(records)
    return _adapter_result("jsonl_replay", ledger, active_runtime)

handle_hierarchy_rest_payload

handle_hierarchy_rest_payload(
    payload: Mapping[str, object],
    *,
    headers: Mapping[str, object],
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult

Handle a decoded REST request payload without owning an HTTP server.

Parameters

payload : Mapping[str, object] The decoded REST request payload. headers : Mapping[str, object] Decoded request headers. runtime : HierarchyTransportRuntime | None The socket-free transport runtime, or None.

Returns

HierarchyAdapterResult The adapter result for the REST payload.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy_adapters.py
def handle_hierarchy_rest_payload(
    payload: Mapping[str, object],
    *,
    headers: Mapping[str, object],
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult:
    """Handle a decoded REST request payload without owning an HTTP server.

    Parameters
    ----------
    payload : Mapping[str, object]
        The decoded REST request payload.
    headers : Mapping[str, object]
        Decoded request headers.
    runtime : HierarchyTransportRuntime | None
        The socket-free transport runtime, or ``None``.

    Returns
    -------
    HierarchyAdapterResult
        The adapter result for the REST payload.
    """
    _require_json_content_type(headers)
    records = _records_from_payload(payload, location="REST payload")
    active_runtime = runtime or HierarchyTransportRuntime()
    ledger = active_runtime.ingest(records)
    return _adapter_result("rest_boundary", ledger, active_runtime)

handle_hierarchy_frame

handle_hierarchy_frame(
    frame: Mapping[str, object],
    *,
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult

Handle a decoded WebSocket-style frame without owning a socket.

Parameters

frame : Mapping[str, object] The decoded WebSocket-style frame. runtime : HierarchyTransportRuntime | None The socket-free transport runtime, or None.

Returns

HierarchyAdapterResult The adapter result for the frame.

Raises

ValueError If the frame is malformed.

Source code in src/scpn_phase_orchestrator/supervisor/hierarchy_adapters.py
def handle_hierarchy_frame(
    frame: Mapping[str, object],
    *,
    runtime: HierarchyTransportRuntime | None = None,
) -> HierarchyAdapterResult:
    """Handle a decoded WebSocket-style frame without owning a socket.

    Parameters
    ----------
    frame : Mapping[str, object]
        The decoded WebSocket-style frame.
    runtime : HierarchyTransportRuntime | None
        The socket-free transport runtime, or ``None``.

    Returns
    -------
    HierarchyAdapterResult
        The adapter result for the frame.

    Raises
    ------
    ValueError
        If the frame is malformed.
    """
    if not isinstance(frame, Mapping):
        raise ValueError("frame must be a decoded mapping")
    _reject_unknown_keys(frame, allowed=_FRAME_KEYS, location="frame")
    frame_kind = _frame_kind(frame)
    payload = frame.get("payload")
    if payload is None:
        raise ValueError("payload must be provided")
    records: tuple[Mapping[str, object] | HierarchySyncEnvelope, ...]
    if frame_kind == _SINGLE_FRAME_KIND:
        records = (_require_record(payload, "frame payload"),)
    elif frame_kind == _BATCH_FRAME_KIND:
        records = _records_from_frame_payload(payload)
    else:
        raise ValueError("frame kind must be hierarchy_sync or hierarchy_sync_batch")
    active_runtime = runtime or HierarchyTransportRuntime()
    ledger = active_runtime.ingest(records)
    return _adapter_result(
        "websocket_frame",
        ledger,
        active_runtime,
        frame_kind=frame_kind,
    )

Byzantine Meta-Orchestrator Manifest

build_bft_meta_orchestrator_manifest() turns signed child-supervisor policy proposals into an offline quorum-review manifest. The manifest records the winning payload hash, accepted and rejected node IDs, hash-linked audit parent, blocked reasons when quorum is absent, and a canonical manifest hash.

The helper verifies HMAC-SHA256 proposal signatures against a supplied keyring, but it does not open network transport or permit direct actuation. Accepted manifests still have to pass the normal supervisor review gate before use.

byzantine

Offline Byzantine-tolerant policy proposal consensus manifests.

Functions:

sign_policy_proposal

sign_policy_proposal(
    node_id: str,
    payload: Mapping[str, object],
    previous_audit_hash: str,
    signing_key: str,
) -> dict[str, object]

Return a deterministic signed policy proposal record.

Parameters

node_id : str Identifier of the proposing node. payload : Mapping[str, object] The policy-proposal payload to sign. previous_audit_hash : str Hash of the previous audit record in the chain. signing_key : str HMAC signing key for the record.

Returns

dict[str, object] The deterministic signed policy-proposal record.

Source code in src/scpn_phase_orchestrator/supervisor/byzantine.py
def sign_policy_proposal(
    node_id: str,
    payload: Mapping[str, object],
    previous_audit_hash: str,
    signing_key: str,
) -> dict[str, object]:
    """Return a deterministic signed policy proposal record.

    Parameters
    ----------
    node_id : str
        Identifier of the proposing node.
    payload : Mapping[str, object]
        The policy-proposal payload to sign.
    previous_audit_hash : str
        Hash of the previous audit record in the chain.
    signing_key : str
        HMAC signing key for the record.

    Returns
    -------
    dict[str, object]
        The deterministic signed policy-proposal record.
    """
    clean_node = _require_text(node_id, "node_id")
    clean_key = _require_text(signing_key, "signing_key")
    _require_hash(previous_audit_hash, "previous_audit_hash")
    payload_hash = _hash_payload(payload)
    signature_payload = _signature_payload(
        clean_node,
        payload_hash,
        previous_audit_hash,
    )
    signature = hmac.new(
        clean_key.encode("utf-8"),
        signature_payload.encode("utf-8"),
        sha256,
    ).hexdigest()
    return {
        "node_id": clean_node,
        "payload": _json_round_trip(payload),
        "payload_hash": payload_hash,
        "previous_audit_hash": previous_audit_hash,
        "signature": signature,
        "signature_algorithm": "hmac-sha256",
    }

build_bft_meta_orchestrator_manifest

build_bft_meta_orchestrator_manifest(
    proposals: Sequence[Mapping[str, object]],
    keyring: Mapping[str, str],
    *,
    quorum: int,
) -> dict[str, object]

Build a review-only three-node BFT consensus manifest.

Parameters

proposals : Sequence[Mapping[str, object]] Signed policy proposals from the participating nodes. keyring : Mapping[str, str] Mapping of node id to its verification key. quorum : int Number of agreeing nodes required for consensus.

Returns

dict[str, object] The review-only BFT consensus manifest.

Raises

ValueError If the proposals fail signature or quorum checks.

Source code in src/scpn_phase_orchestrator/supervisor/byzantine.py
def build_bft_meta_orchestrator_manifest(
    proposals: Sequence[Mapping[str, object]],
    keyring: Mapping[str, str],
    *,
    quorum: int,
) -> dict[str, object]:
    """Build a review-only three-node BFT consensus manifest.

    Parameters
    ----------
    proposals : Sequence[Mapping[str, object]]
        Signed policy proposals from the participating nodes.
    keyring : Mapping[str, str]
        Mapping of node id to its verification key.
    quorum : int
        Number of agreeing nodes required for consensus.

    Returns
    -------
    dict[str, object]
        The review-only BFT consensus manifest.

    Raises
    ------
    ValueError
        If the proposals fail signature or quorum checks.
    """
    if quorum < 1:
        raise ValueError("quorum must be >= 1")
    if isinstance(proposals, Mapping) or not proposals:
        raise ValueError("proposals must be a non-empty sequence")
    if isinstance(keyring, Sequence) or not keyring:
        raise ValueError("keyring must be a non-empty mapping")

    verified: list[dict[str, object]] = []
    rejected: list[dict[str, object]] = []
    blocked_reasons: list[str] = []
    seen_nodes: set[str] = set()
    for proposal in proposals:
        record = _verify_proposal(proposal, keyring)
        node_id = str(record["node_id"])
        if node_id in seen_nodes:
            record["valid"] = False
            record["reason"] = f"{node_id} duplicate proposal"
        seen_nodes.add(node_id)
        if record["valid"] is True:
            verified.append(record)
        else:
            rejected.append(record)
            blocked_reasons.append(str(record["reason"]))

    accepted_group = _accepted_quorum_group(verified, quorum)
    if accepted_group is None:
        accepted_group = []
        blocked_reasons.append("valid quorum not reached")
    accepted_node_ids = sorted(str(record["node_id"]) for record in accepted_group)
    accepted_node_set = set(accepted_node_ids)
    non_winning = (
        [
            str(record["node_id"])
            for record in verified
            if str(record["node_id"]) not in accepted_node_set
        ]
        if accepted_node_ids
        else []
    )
    rejected_node_ids = sorted(
        [*(str(record["node_id"]) for record in rejected), *non_winning]
    )
    consensus_hash = str(accepted_group[0]["payload_hash"]) if accepted_group else ""
    audit_chain_hash = _audit_chain_hash(accepted_group)
    manifest: dict[str, object] = {
        "manifest_kind": "bft_meta_orchestrator_manifest",
        "schema_version": 1,
        "status": "accepted" if accepted_group else "blocked",
        "quorum": quorum,
        "node_count": len(proposals),
        "accepted_node_ids": accepted_node_ids,
        "rejected_node_ids": rejected_node_ids,
        "consensus_hash": consensus_hash,
        "audit_chain_hash": audit_chain_hash,
        "blocked_reasons": _dedupe(blocked_reasons),
        "actuation_permitted": False,
        "network_opened": False,
        "operator_commands": [
            "review bft_meta_orchestrator_manifest.json",
            "apply accepted policy only through the normal supervisor review gate",
        ],
    }
    canonical = json.dumps(manifest, sort_keys=True, separators=(",", ":"))
    manifest["manifest_sha256"] = sha256(canonical.encode("utf-8")).hexdigest()
    return manifest

Strange-Loop Supervisor Monitor

StrangeLoopSupervisor is the first self-referential supervisor slice. It treats the supervisor's own action stream as a four-dimensional control channel over K, alpha, zeta, and Psi. The monitor records recent action bundles, computes a control phase, control coherence, drift score, oscillation score, and over-control score, then returns conservative damping recommendations for a normal policy or safety gate to approve.

from scpn_phase_orchestrator.supervisor import StrangeLoopSupervisor

loop = StrangeLoopSupervisor(overcontrol_threshold=0.2)
assessment = loop.observe(actions_from_supervisor_policy)

if assessment.recommended_actions:
    audit_payload = assessment.to_audit_record()

This slice does not hot-patch the supervisor or claim autonomous self-awareness. It provides an auditable meta-control signal that can detect policy drift, control-loop oscillation, and excessive actuation before those dynamics are fed back into the plant.

Long-run drift scenario helpers exercise that monitor across deterministic 40-step review traces for stable power-grid trims, cardiac policy drift, traffic-control oscillation, and plasma over-control. The fixture corpus stays non-actuating and execution-disabled, publishes stable scenario/result hashes, and is gated in the reference suite so drift, oscillation, and over-control threshold behavior remains reproducible across releases. Studio renders the resulting audit records through the public scpn_phase_orchestrator.studio.build_strange_loop_studio_panel() facade, which preserves the strange_loop_drift_review_not_live_actuation boundary, validates SHA-256 evidence hashes and finite metric ranges, and keeps all recommendations behind the normal review and safety gate.

strange_loop

Self-monitoring supervisor action-history diagnostics.

StrangeLoopSupervisor embeds recent control-action bundles into native control-knob space and measures drift, oscillation, coherence, and over-control from the bounded history. Recommendations are conservative damping proposals for an outer policy gate to approve. The monitor records diagnostics only and does not apply actions or alter the underlying supervisor.

Classes

StrangeLoopDriftScenario dataclass

StrangeLoopDriftScenario(
    domain: str,
    scenario_id: str,
    description: str,
    expected_trigger: str,
    action_schedule: tuple[tuple[ControlAction, ...], ...],
    non_actuating: bool = True,
    execution_disabled: bool = True,
    claim_boundary: str = _DRIFT_SCENARIO_BOUNDARY,
)

Deterministic long-run action-history scenario for strange-loop review.

Methods:
scenario_hash
scenario_hash() -> str

Return a deterministic scenario hash over the full action schedule.

Returns

str Return a deterministic scenario hash over the full action schedule.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def scenario_hash(self) -> str:
    """Return a deterministic scenario hash over the full action schedule.

    Returns
    -------
    str
        Return a deterministic scenario hash over the full action schedule.
    """
    return _stable_hash(_scenario_payload(self))
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe long-run scenario record.

Returns

dict[str, object] Return a JSON-safe long-run scenario record.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe long-run scenario record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe long-run scenario record.
    """
    return {
        "domain": self.domain,
        "scenario_id": self.scenario_id,
        "description": self.description,
        "expected_trigger": self.expected_trigger,
        "step_count": len(self.action_schedule),
        "scenario_hash": self.scenario_hash(),
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
        "action_schedule": [
            [_action_to_record(action) for action in bundle]
            for bundle in self.action_schedule
        ],
    }

StrangeLoopDriftScenarioResult dataclass

StrangeLoopDriftScenarioResult(
    domain: str,
    scenario_id: str,
    expected_trigger: str,
    step_count: int,
    max_drift_score: float,
    max_oscillation_score: float,
    max_overcontrol_score: float,
    min_control_coherence: float,
    triggered_recommendation_count: int,
    final_recommended_knobs: tuple[str, ...],
    passed_expected_trigger: bool,
    scenario_hash: str,
    result_hash: str,
    non_actuating: bool = True,
    execution_disabled: bool = True,
    claim_boundary: str = _DRIFT_SCENARIO_BOUNDARY,
)

Audit-ready result for one long-run strange-loop drift scenario.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe drift scenario result.

Returns

dict[str, object] Return a JSON-safe drift scenario result.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe drift scenario result.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe drift scenario result.
    """
    return {
        "domain": self.domain,
        "scenario_id": self.scenario_id,
        "expected_trigger": self.expected_trigger,
        "step_count": self.step_count,
        "max_drift_score": self.max_drift_score,
        "max_oscillation_score": self.max_oscillation_score,
        "max_overcontrol_score": self.max_overcontrol_score,
        "min_control_coherence": self.min_control_coherence,
        "triggered_recommendation_count": self.triggered_recommendation_count,
        "final_recommended_knobs": list(self.final_recommended_knobs),
        "passed_expected_trigger": self.passed_expected_trigger,
        "scenario_hash": self.scenario_hash,
        "result_hash": self.result_hash,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
    }

StrangeLoopAssessment dataclass

StrangeLoopAssessment(
    control_phase: float,
    control_coherence: float,
    drift_score: float,
    oscillation_score: float,
    overcontrol_score: float,
    recommended_actions: tuple[ControlAction, ...],
)

Audit-ready metrics for supervisor self-control dynamics.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable record for supervisor audit logs.

Returns

dict[str, object] Return a JSON-serialisable record for supervisor audit logs.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable record for supervisor audit logs.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable record for supervisor audit logs.
    """
    return {
        "control_phase": self.control_phase,
        "control_coherence": self.control_coherence,
        "drift_score": self.drift_score,
        "oscillation_score": self.oscillation_score,
        "overcontrol_score": self.overcontrol_score,
        "recommended_actions": [
            {
                "knob": action.knob,
                "scope": action.scope,
                "value": action.value,
                "ttl_s": action.ttl_s,
                "justification": action.justification,
            }
            for action in self.recommended_actions
        ],
    }

StrangeLoopSupervisor

StrangeLoopSupervisor(
    *,
    history_size: int = 12,
    drift_threshold: float = 0.25,
    oscillation_threshold: float = 0.5,
    overcontrol_threshold: float = 0.2,
    damping_gain: float = 0.05,
    ttl_s: float = 3.0,
)

Treat supervisor action history as a self-referential control channel.

The monitor embeds each recent action bundle into the four native control knobs (K, alpha, zeta, Psi). It then measures whether the supervisor is drifting, oscillating, or over-actuating and emits conservative damping recommendations for an outer policy gate to approve.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def __init__(
    self,
    *,
    history_size: int = 12,
    drift_threshold: float = 0.25,
    oscillation_threshold: float = 0.5,
    overcontrol_threshold: float = 0.2,
    damping_gain: float = 0.05,
    ttl_s: float = 3.0,
) -> None:
    if (
        isinstance(history_size, (bool, np.bool_))
        or not isinstance(history_size, Integral)
        or history_size < 2
    ):
        raise ValueError("history_size must be an integer >= 2")
    self._history: deque[FloatArray] = deque(maxlen=int(history_size))
    self._drift_threshold: float = _require_positive_real(
        drift_threshold, name="drift_threshold"
    )
    self._oscillation_threshold: float = _require_positive_real(
        oscillation_threshold, name="oscillation_threshold"
    )
    self._overcontrol_threshold: float = _require_positive_real(
        overcontrol_threshold, name="overcontrol_threshold"
    )
    self._damping_gain: float = _require_positive_real(
        damping_gain, name="damping_gain"
    )
    self._ttl_s: float = _require_positive_real(ttl_s, name="ttl_s")
    self.last_assessment: StrangeLoopAssessment | None = None
Methods:
observe
observe(
    actions: list[ControlAction],
) -> StrangeLoopAssessment

Record one supervisor action bundle and assess self-control state.

Parameters

actions : list[ControlAction] The control actions to apply or assess.

Returns

StrangeLoopAssessment The self-control assessment for the action bundle.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def observe(self, actions: list[ControlAction]) -> StrangeLoopAssessment:
    """Record one supervisor action bundle and assess self-control state.

    Parameters
    ----------
    actions : list[ControlAction]
        The control actions to apply or assess.

    Returns
    -------
    StrangeLoopAssessment
        The self-control assessment for the action bundle.
    """
    vector = _actions_to_vector(actions)
    self._history.append(vector)
    assessment = self._assess()
    self.last_assessment = assessment
    return assessment
reset
reset() -> None

Clear action history and the cached assessment.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def reset(self) -> None:
    """Clear action history and the cached assessment."""
    self._history.clear()
    self.last_assessment = None

Functions:

build_strange_loop_drift_scenarios

build_strange_loop_drift_scenarios() -> tuple[
    StrangeLoopDriftScenario, ...
]

Build deterministic long-run strange-loop drift review scenarios.

Returns

tuple[StrangeLoopDriftScenario, ...] Build deterministic long-run strange-loop drift review scenarios.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def build_strange_loop_drift_scenarios() -> tuple[StrangeLoopDriftScenario, ...]:
    """Build deterministic long-run strange-loop drift review scenarios.

    Returns
    -------
    tuple[StrangeLoopDriftScenario, ...]
        Build deterministic long-run strange-loop drift review scenarios.
    """
    scenarios = (
        StrangeLoopDriftScenario(
            domain="power_grid",
            scenario_id="strange_loop_stable_frequency_trim_v1",
            description=(
                "Stable small frequency-regulation nudges should stay below "
                "drift, oscillation, and over-control review thresholds."
            ),
            expected_trigger="stable",
            action_schedule=tuple(
                (_review_action("K", 0.02, step),) for step in range(40)
            ),
        ),
        StrangeLoopDriftScenario(
            domain="cardiac_rhythm",
            scenario_id="strange_loop_monotone_policy_drift_v1",
            description=(
                "A monotone escalation of coupling proposals should be flagged "
                "as policy drift before it could become live actuation."
            ),
            expected_trigger="policy_drift",
            action_schedule=tuple(
                (_review_action("K", 0.03 + 0.12 * step, step),) for step in range(40)
            ),
        ),
        StrangeLoopDriftScenario(
            domain="traffic_flow",
            scenario_id="strange_loop_alternating_control_v1",
            description=(
                "Alternating sign control proposals should expose control-loop "
                "oscillation in the action-history embedding."
            ),
            expected_trigger="control_loop_oscillation",
            action_schedule=tuple(
                (_review_action("K", 0.22 if step % 2 == 0 else -0.22, step),)
                for step in range(40)
            ),
        ),
        StrangeLoopDriftScenario(
            domain="plasma_control",
            scenario_id="strange_loop_sustained_overcontrol_v1",
            description=(
                "Sustained large but non-oscillatory proposals should trigger "
                "over-control recommendations even when drift is low."
            ),
            expected_trigger="over_control",
            action_schedule=tuple(
                (
                    _review_action("K", 0.52, step),
                    _review_action("zeta", 0.08, step),
                )
                for step in range(40)
            ),
        ),
    )
    for scenario in scenarios:
        _validate_drift_scenario(scenario)
    return scenarios

evaluate_strange_loop_drift_scenarios

evaluate_strange_loop_drift_scenarios(
    scenarios: Sequence[StrangeLoopDriftScenario]
    | None = None,
) -> tuple[StrangeLoopDriftScenarioResult, ...]

Evaluate long-run drift scenarios through StrangeLoopSupervisor.

Parameters

scenarios : Sequence[StrangeLoopDriftScenario] | None The drift scenarios to evaluate, or None for the defaults.

Returns

tuple[StrangeLoopDriftScenarioResult, ...] The drift-scenario results.

Source code in src/scpn_phase_orchestrator/supervisor/strange_loop.py
def evaluate_strange_loop_drift_scenarios(
    scenarios: Sequence[StrangeLoopDriftScenario] | None = None,
) -> tuple[StrangeLoopDriftScenarioResult, ...]:
    """Evaluate long-run drift scenarios through ``StrangeLoopSupervisor``.

    Parameters
    ----------
    scenarios : Sequence[StrangeLoopDriftScenario] | None
        The drift scenarios to evaluate, or ``None`` for the defaults.

    Returns
    -------
    tuple[StrangeLoopDriftScenarioResult, ...]
        The drift-scenario results.
    """
    scenario_tuple = (
        build_strange_loop_drift_scenarios() if scenarios is None else tuple(scenarios)
    )
    results: list[StrangeLoopDriftScenarioResult] = []
    for scenario in scenario_tuple:
        _validate_drift_scenario(scenario)
        supervisor = StrangeLoopSupervisor(
            history_size=16,
            drift_threshold=_SCENARIO_DRIFT_THRESHOLD,
            oscillation_threshold=_SCENARIO_OSCILLATION_THRESHOLD,
            overcontrol_threshold=_SCENARIO_OVERCONTROL_THRESHOLD,
        )
        assessments = [
            supervisor.observe(list(bundle)) for bundle in scenario.action_schedule
        ]
        max_drift = max(assessment.drift_score for assessment in assessments)
        max_oscillation = max(
            assessment.oscillation_score for assessment in assessments
        )
        max_overcontrol = max(
            assessment.overcontrol_score for assessment in assessments
        )
        min_coherence = min(assessment.control_coherence for assessment in assessments)
        triggered_count = sum(
            1 for assessment in assessments if assessment.recommended_actions
        )
        final_knobs = tuple(
            action.knob for action in assessments[-1].recommended_actions
        )
        passed = _passes_expected_trigger(
            expected_trigger=scenario.expected_trigger,
            max_drift_score=max_drift,
            max_oscillation_score=max_oscillation,
            max_overcontrol_score=max_overcontrol,
            triggered_recommendation_count=triggered_count,
        )
        scenario_hash = scenario.scenario_hash()
        result_payload = {
            "domain": scenario.domain,
            "scenario_id": scenario.scenario_id,
            "expected_trigger": scenario.expected_trigger,
            "step_count": len(scenario.action_schedule),
            "max_drift_score": max_drift,
            "max_oscillation_score": max_oscillation,
            "max_overcontrol_score": max_overcontrol,
            "min_control_coherence": min_coherence,
            "triggered_recommendation_count": triggered_count,
            "final_recommended_knobs": list(final_knobs),
            "passed_expected_trigger": passed,
            "scenario_hash": scenario_hash,
            "non_actuating": True,
            "execution_disabled": True,
            "claim_boundary": _DRIFT_SCENARIO_BOUNDARY,
        }
        results.append(
            StrangeLoopDriftScenarioResult(
                domain=scenario.domain,
                scenario_id=scenario.scenario_id,
                expected_trigger=scenario.expected_trigger,
                step_count=len(scenario.action_schedule),
                max_drift_score=float(max_drift),
                max_oscillation_score=float(max_oscillation),
                max_overcontrol_score=float(max_overcontrol),
                min_control_coherence=float(min_coherence),
                triggered_recommendation_count=triggered_count,
                final_recommended_knobs=final_knobs,
                passed_expected_trigger=passed,
                scenario_hash=scenario_hash,
                result_hash=_stable_hash(result_payload),
            )
        )
    return tuple(results)

Morphogenetic Topology Field

MorphogeneticTopologySupervisor evolves a persistent field over the pairwise coupling topology. Each step combines:

  • pairwise phase-alignment reaction terms
  • incident-edge diffusion over the current topology field
  • bounded growth and shrink rates
  • a hard maximum per-step coupling delta

The result is a next-step K_nm, a carried MorphogeneticFieldState, grown and shrunk edge lists, and compact field statistics for audit logs.

from scpn_phase_orchestrator.supervisor import (
    MorphogeneticTopologySupervisor,
    build_morphogenetic_field_snapshot,
    render_morphogenetic_field_svg,
)

supervisor = MorphogeneticTopologySupervisor()
result = supervisor.step(phases, knm)

next_knm = result.knm
field_state = result.field_state
audit_payload = result.to_audit_record()
snapshot = build_morphogenetic_field_snapshot(result, top_k=5)
heatmap_rows = snapshot.heatmap_rows
svg_artifact = render_morphogenetic_field_svg(result, top_k=5)

This slice provides a reviewable grow/shrink primitive for topology shaping. It does not bypass the existing policy, causal, STL, or action-projection gates. The field snapshot helper is dependency-free and emits JSON-safe statistics, ASCII heatmap rows, and strongest-edge records for reports or later UI rendering. Coupling and carried topology-field matrices are strict off-diagonal graph objects: boolean and complex aliases are rejected before float coercion, and non-zero self-edge diagonals are rejected before any field evolution, snapshot, or SVG rendering.

render_morphogenetic_field_svg() is the first richer UI rendering surface for the same field state. It produces a deterministic, dependency-free SVG heatmap plus top-edge labels and snapshot metadata. The renderer is passive: it turns an already computed field into a review artefact and does not mutate policy, coupling, or actuation state. Studio packages those SVG artefacts through the public scpn_phase_orchestrator.studio.build_morphogenetic_field_studio_panel() facade, which validates complete SVG documents, fixed-width heatmap rows, field-energy statistics, and sorted off-diagonal topology edges before exposing the panel as passive operator evidence.

domainpacks/swarm_robotics/morphogenetic_field_demo.py provides a deterministic domainpack proof: it evaluates a split-flock phase state and emits the morphogenetic field audit payload plus snapshot rows without live actuation.

domainpacks/power_grid/morphogenetic_field_demo.py provides the same non-actuating proof for a stressed grid replay: generator rotor and area frequency layers remain near-synchronised while tie-line, load-demand, and renewable layers drift, producing reviewable grown/shrunk field-edge records.

domainpacks/traffic_flow/morphogenetic_field_demo.py extends the demo set with a corridor spillback replay: corridor, network, and equity-pressure layers remain locally aligned while intersection, demand, and weather phases stress the field, again without live actuation.

domainpacks/plasma_control/morphogenetic_field_demo.py adds a research plasma replay: transport-barrier, current-profile, and global-equilibrium layers remain locally aligned while turbulence, tearing, ELM, and wall-interaction phases stress the field, again without live actuation.

domainpacks/network_security/morphogenetic_field_demo.py adds a lateral-movement replay: normal-traffic and defence-response layers remain locally aligned while the attack-vector layer stresses the field, again without live actuation.

morphogenetic

Morphogenetic topology-field diagnostics for bounded coupling proposals.

The supervisor evolves a persistent normalized field from phase alignment, diffusion, and coherence-target reactions, then returns a clipped coupling proposal plus audit summaries of grown and shrunk edges. Snapshot and SVG helpers render review artifacts from computed fields. The module does not apply coupling updates to external systems or perform actuation.

Classes

MorphogeneticFieldPolicy dataclass

MorphogeneticFieldPolicy(
    growth_rate: float = 0.2,
    shrink_rate: float = 0.15,
    diffusion_rate: float = 0.1,
    coherence_target: float = 0.75,
    max_delta: float = 0.05,
    max_coupling: float = 10.0,
)

Knobs for reaction-diffusion-style topology field evolution.

MorphogeneticFieldState dataclass

MorphogeneticFieldState(field: FloatArray)

Persistent topology field carried between supervisor ticks.

Methods:
to_audit_snapshot
to_audit_snapshot() -> dict[str, object]

Return compact, serialisable field statistics for audit logs.

Returns

dict[str, object] Return compact, serialisable field statistics for audit logs.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def to_audit_snapshot(self) -> dict[str, object]:
    """Return compact, serialisable field statistics for audit logs.

    Returns
    -------
    dict[str, object]
        Return compact, serialisable field statistics for audit logs.
    """
    return {
        "shape": list(self.field.shape),
        "mean": float(np.mean(self.field)),
        "minimum": float(np.min(self.field)),
        "maximum": float(np.max(self.field)),
        "l2_norm": float(np.linalg.norm(self.field)),
    }

MorphogeneticFieldResult dataclass

MorphogeneticFieldResult(
    knm: FloatArray,
    field_state: MorphogeneticFieldState,
    grown_edges: tuple[tuple[int, int, float], ...],
    shrunk_edges: tuple[tuple[int, int, float], ...],
    delta_norm: float,
    global_coherence: float,
)

Output of one morphogenetic topology field step.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable topology-field audit payload.

Returns

dict[str, object] Return a serialisable topology-field audit payload.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable topology-field audit payload.

    Returns
    -------
    dict[str, object]
        Return a serialisable topology-field audit payload.
    """
    return {
        "global_coherence": self.global_coherence,
        "delta_norm": self.delta_norm,
        "grown_edges": [
            {"source": src, "target": dst, "delta": delta}
            for src, dst, delta in self.grown_edges
        ],
        "shrunk_edges": [
            {"source": src, "target": dst, "delta": delta}
            for src, dst, delta in self.shrunk_edges
        ],
        "field": self.field_state.to_audit_snapshot(),
    }

MorphogeneticFieldSnapshot dataclass

MorphogeneticFieldSnapshot(
    shape: tuple[int, int],
    mean: float,
    minimum: float,
    maximum: float,
    l2_norm: float,
    heatmap_rows: tuple[str, ...],
    top_edges: tuple[tuple[int, int, float], ...],
)

Compact visual snapshot of a morphogenetic topology field.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe field snapshot for docs, reports, and audits.

Returns

dict[str, object] Return a JSON-safe field snapshot for docs, reports, and audits.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe field snapshot for docs, reports, and audits.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe field snapshot for docs, reports, and audits.
    """
    return {
        "shape": list(self.shape),
        "mean": self.mean,
        "minimum": self.minimum,
        "maximum": self.maximum,
        "l2_norm": self.l2_norm,
        "heatmap_rows": list(self.heatmap_rows),
        "top_edges": [
            {"source": src, "target": dst, "weight": weight}
            for src, dst, weight in self.top_edges
        ],
    }

MorphogeneticFieldSVG dataclass

MorphogeneticFieldSVG(
    svg: str,
    width: int,
    height: int,
    snapshot: MorphogeneticFieldSnapshot,
)

Dependency-free SVG rendering of a morphogenetic topology field.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe SVG artefact record for review tooling.

Returns

dict[str, object] Return a JSON-safe SVG artefact record for review tooling.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe SVG artefact record for review tooling.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe SVG artefact record for review tooling.
    """
    return {
        "format": "svg",
        "width": self.width,
        "height": self.height,
        "snapshot": self.snapshot.to_audit_record(),
        "svg": self.svg,
    }

MorphogeneticTopologySupervisor

MorphogeneticTopologySupervisor(
    policy: MorphogeneticFieldPolicy | None = None,
)

Grow or shrink pairwise topology from a persistent coherence field.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def __init__(self, policy: MorphogeneticFieldPolicy | None = None) -> None:
    self.policy = policy or MorphogeneticFieldPolicy()
    self.last_result: MorphogeneticFieldResult | None = None
Methods:
step
step(
    phases: FloatArray,
    knm: FloatArray,
    field_state: MorphogeneticFieldState | None = None,
) -> MorphogeneticFieldResult

Evolve the topology field and return the next pairwise coupling.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). knm : FloatArray Coupling matrix K_nm, shape (N, N). field_state : MorphogeneticFieldState | None The morphogenetic field state, or None.

Returns

MorphogeneticFieldResult The next pairwise coupling field result.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def step(
    self,
    phases: FloatArray,
    knm: FloatArray,
    field_state: MorphogeneticFieldState | None = None,
) -> MorphogeneticFieldResult:
    """Evolve the topology field and return the next pairwise coupling.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    field_state : MorphogeneticFieldState | None
        The morphogenetic field state, or ``None``.

    Returns
    -------
    MorphogeneticFieldResult
        The next pairwise coupling field result.
    """
    phases_arr = _validate_phases(phases)
    knm_arr = _validate_knm(knm, phases_arr.size)
    field = (
        _initial_field(knm_arr, self.policy.max_coupling)
        if field_state is None
        else _validate_field(field_state.field, phases_arr.size)
    )
    alignment = _pairwise_phase_alignment(phases_arr)
    diffused = _incident_diffusion(field)
    reaction = alignment - self.policy.coherence_target
    next_field = np.clip(
        field
        + self.policy.diffusion_rate * (diffused - field)
        + self.policy.growth_rate * np.maximum(reaction, 0.0)
        - self.policy.shrink_rate * np.maximum(-reaction, 0.0),
        0.0,
        1.0,
    )
    np.fill_diagonal(next_field, 0.0)
    field_delta = next_field - field
    coupling_delta = self.policy.max_delta * field_delta
    next_knm = np.clip(knm_arr + coupling_delta, 0.0, self.policy.max_coupling)
    np.fill_diagonal(next_knm, 0.0)
    result = MorphogeneticFieldResult(
        knm=next_knm,
        field_state=MorphogeneticFieldState(next_field),
        grown_edges=_edge_deltas(coupling_delta, positive=True),
        shrunk_edges=_edge_deltas(coupling_delta, positive=False),
        delta_norm=float(np.linalg.norm(next_knm - knm_arr)),
        global_coherence=_order_parameter(phases_arr),
    )
    self.last_result = result
    return result
reset
reset() -> None

Clear the cached result; caller owns persistent field snapshots.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def reset(self) -> None:
    """Clear the cached result; caller owns persistent field snapshots."""
    self.last_result = None

Functions:

build_morphogenetic_field_snapshot

build_morphogenetic_field_snapshot(
    field_state: MorphogeneticFieldState
    | MorphogeneticFieldResult,
    *,
    top_k: int = 5,
    palette: str = " .:-=+*#%@",
) -> MorphogeneticFieldSnapshot

Build a compact visual snapshot for a topology field.

The snapshot is dependency-free and audit-oriented: it exposes summary statistics, ASCII heatmap rows, and the strongest non-diagonal field edges.

Parameters

field_state : MorphogeneticFieldState | MorphogeneticFieldResult The morphogenetic field state, or None. top_k : int Number of strongest entries to retain. palette : str Colour palette name for the snapshot.

Returns

MorphogeneticFieldSnapshot The compact morphogenetic field snapshot.

Raises

ValueError If the field state or top_k is invalid.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def build_morphogenetic_field_snapshot(
    field_state: MorphogeneticFieldState | MorphogeneticFieldResult,
    *,
    top_k: int = 5,
    palette: str = " .:-=+*#%@",
) -> MorphogeneticFieldSnapshot:
    """Build a compact visual snapshot for a topology field.

    The snapshot is dependency-free and audit-oriented: it exposes summary
    statistics, ASCII heatmap rows, and the strongest non-diagonal field edges.

    Parameters
    ----------
    field_state : MorphogeneticFieldState | MorphogeneticFieldResult
        The morphogenetic field state, or ``None``.
    top_k : int
        Number of strongest entries to retain.
    palette : str
        Colour palette name for the snapshot.

    Returns
    -------
    MorphogeneticFieldSnapshot
        The compact morphogenetic field snapshot.

    Raises
    ------
    ValueError
        If the field state or ``top_k`` is invalid.
    """
    if isinstance(top_k, (bool, np.bool_)) or not isinstance(top_k, int) or top_k < 0:
        raise ValueError("top_k must be non-negative")
    _require_non_empty(palette, "palette")
    source_state = (
        field_state.field_state
        if isinstance(field_state, MorphogeneticFieldResult)
        else field_state
    )
    field = _validate_square_field(source_state.field)
    return MorphogeneticFieldSnapshot(
        shape=(int(field.shape[0]), int(field.shape[1])),
        mean=float(np.mean(field)),
        minimum=float(np.min(field)),
        maximum=float(np.max(field)),
        l2_norm=float(np.linalg.norm(field)),
        heatmap_rows=_field_heatmap_rows(field, palette),
        top_edges=_top_field_edges(field, top_k),
    )

render_morphogenetic_field_svg

render_morphogenetic_field_svg(
    field_state: MorphogeneticFieldState
    | MorphogeneticFieldResult,
    *,
    top_k: int = 5,
    cell_size: int = 28,
    title: str = "Morphogenetic topology field",
) -> MorphogeneticFieldSVG

Render a dependency-free SVG heatmap for a topology field.

The renderer is passive: it produces a review artefact from an already computed field and does not mutate policy, coupling, or actuation state.

Parameters

field_state : MorphogeneticFieldState | MorphogeneticFieldResult The morphogenetic field state, or None. top_k : int Number of strongest entries to retain. cell_size : int SVG cell size in pixels. title : str Title rendered on the SVG.

Returns

MorphogeneticFieldSVG The dependency-free SVG heatmap artefact.

Raises

ValueError If the field state or rendering parameters are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/morphogenetic.py
def render_morphogenetic_field_svg(
    field_state: MorphogeneticFieldState | MorphogeneticFieldResult,
    *,
    top_k: int = 5,
    cell_size: int = 28,
    title: str = "Morphogenetic topology field",
) -> MorphogeneticFieldSVG:
    """Render a dependency-free SVG heatmap for a topology field.

    The renderer is passive: it produces a review artefact from an already
    computed field and does not mutate policy, coupling, or actuation state.

    Parameters
    ----------
    field_state : MorphogeneticFieldState | MorphogeneticFieldResult
        The morphogenetic field state, or ``None``.
    top_k : int
        Number of strongest entries to retain.
    cell_size : int
        SVG cell size in pixels.
    title : str
        Title rendered on the SVG.

    Returns
    -------
    MorphogeneticFieldSVG
        The dependency-free SVG heatmap artefact.

    Raises
    ------
    ValueError
        If the field state or rendering parameters are invalid.
    """
    if (
        isinstance(cell_size, (bool, np.bool_))
        or not isinstance(cell_size, int)
        or cell_size < 8
    ):
        raise ValueError("cell_size must be at least 8")
    _require_non_empty(title, "title")
    source_state = (
        field_state.field_state
        if isinstance(field_state, MorphogeneticFieldResult)
        else field_state
    )
    field = _validate_square_field(source_state.field)
    snapshot = build_morphogenetic_field_snapshot(source_state, top_k=top_k)
    n = int(field.shape[0])
    label_band = 84
    legend_band = 24
    width = cell_size * n
    height = label_band + cell_size * n + legend_band
    escaped_title = escape(title, quote=True)
    parts = [
        (
            f'<svg xmlns="http://www.w3.org/2000/svg" role="img" '
            f'viewBox="0 0 {width} {height}" width="{width}" height="{height}">'
        ),
        f"<title>{escaped_title}</title>",
        f'<rect width="{width}" height="{height}" fill="#fbf7ef"/>',
        (
            f'<text x="0" y="18" font-family="monospace" font-size="13" '
            f'fill="#24302f">{escaped_title}</text>'
        ),
        (
            f'<text x="0" y="38" font-family="monospace" font-size="11" '
            f'fill="#5f6b64">mean={snapshot.mean:.4f} '
            f"max={snapshot.maximum:.4f} l2={snapshot.l2_norm:.4f}</text>"
        ),
    ]
    y0 = label_band
    for row_idx, row in enumerate(field):
        for col_idx, value in enumerate(row):
            opacity = 0.10 + 0.85 * float(value)
            x = col_idx * cell_size
            y = y0 + row_idx * cell_size
            parts.append(
                f'<rect x="{x}" y="{y}" width="{cell_size}" height="{cell_size}" '
                f'fill="#0e7c66" fill-opacity="{opacity:.4f}" '
                f'stroke="#f3eadc" stroke-width="1"/>'
            )
    for src, dst, weight in snapshot.top_edges:
        label_x = dst * cell_size + cell_size / 2.0
        label_y = y0 + src * cell_size + cell_size / 2.0
        parts.append(
            f'<text x="{label_x:.1f}" y="{label_y + 3.5:.1f}" text-anchor="middle" '
            f'font-family="monospace" font-size="{max(8, cell_size // 3)}" '
            f'fill="#10221f">{src}->{dst}:{weight:.2f}</text>'
        )
    parts.append(
        f'<text x="0" y="{height - 7}" font-family="monospace" font-size="10" '
        f'fill="#5f6b64">top_edges={len(snapshot.top_edges)} '
        f"shape={snapshot.shape[0]}x{snapshot.shape[1]}</text>"
    )
    parts.append("</svg>")
    return MorphogeneticFieldSVG(
        svg="".join(parts),
        width=width,
        height=height,
        snapshot=snapshot,
    )

Sheaf Coherence Supervisor

SheafCoherenceSupervisor evaluates N-channel node states against directed restriction maps. It builds the block sheaf Laplacian, computes edge residuals, and reports obstruction metrics for audit logs.

Inputs are fail-closed real-valued tensors: node_states must have shape (n_nodes, n_channels) and restriction_maps must have shape (n_nodes, n_nodes, n_channels, n_channels). Boolean aliases, complex values, non-finite values, and malformed object payloads are rejected before Laplacian assembly so the obstruction score cannot depend on implicit dtype coercion.

This supervisor-facing sheaf-cohomology slice exposes obstruction score, consistency energy, approximate kernel dimension, obstruction dimension, and a review-only obstruction-aware control primitive. It does not claim a complete formal proof system or autonomous sheaf-control loop.

from scpn_phase_orchestrator.supervisor import (
    SheafCoherenceSupervisor,
    build_sheaf_obstruction_summary,
)

supervisor = SheafCoherenceSupervisor(tolerance=1e-8)
result = supervisor.assess(node_states, restriction_maps)
summary = build_sheaf_obstruction_summary(result)

if result.obstruction_score > 0.1:
    audit_payload = summary.to_audit_record()

propose_sheaf_obstruction_control() projects an obstructed section one bounded step down the sheaf-Laplacian consistency-energy gradient. The use case is operator review: identify a mathematically justified state correction that reduces obstruction while recording before/after cohomology dimensions. The proposal is always non-actuating, execution-disabled, and review-required.

from scpn_phase_orchestrator.supervisor import (
    propose_sheaf_obstruction_control,
)

proposal = propose_sheaf_obstruction_control(
    node_states,
    restriction_maps,
    step_size=0.25,
    max_update_norm=0.4,
)
assert proposal.projected_consistency_energy <= proposal.baseline_consistency_energy
assert proposal.execution_disabled

domainpacks/edge_consensus_nchannel/sheaf_obstruction_demo.py provides a heterogeneous-domain replay: P, I, S, Load, Trust, and ConsensusHealth node states are evaluated across edge, gateway, and parent restriction maps, producing nominal and stressed obstruction audit records without live actuation.

domainpacks/power_grid/sheaf_obstruction_demo.py adds a second heterogeneous-domain replay. It evaluates generation, tie-line, load, and renewable regions over rotor-angle, frequency-deviation, tie-flow, demand, and renewable-ramp channels, then reports nominal versus line-fault obstruction summaries.

domainpacks/network_security/sheaf_obstruction_demo.py adds a security replay. It evaluates normal-traffic, attack-vector, and defence-response cohorts over traffic-rate, threat-level, defence-phase, and trust-score channels, then reports nominal versus lateral-movement obstruction summaries.

build_sheaf_obstruction_summary() hardens the raw obstruction metric into a reviewable triage record. It classifies nominal, warning, and critical states from explicit thresholds and reports the strongest residual edges so operators can see which directed restrictions are failing.

Studio exposes this evidence through build_sheaf_cohomology_studio_panel(records, summaries, control_proposals). That panel keeps obstruction records, residual-edge summaries, and bounded review-only control proposals together while preserving disabled execution and actuation gates.

sheaf

Sheaf-Laplacian coherence assessment for N-channel supervisor states.

Classes

SheafCoherenceResult dataclass

SheafCoherenceResult(
    laplacian: FloatArray,
    residuals: FloatArray,
    obstruction_score: float,
    consistency_energy: float,
    kernel_dimension: int,
    obstruction_dimension: int,
    edge_count: int,
    tolerance: float,
)

Audit-ready obstruction assessment for a cellular-sheaf state.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a compact serialisable payload for supervisor audit logs.

Returns

dict[str, object] Return a compact serialisable payload for supervisor audit logs.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def to_audit_record(self) -> dict[str, object]:
    """Return a compact serialisable payload for supervisor audit logs.

    Returns
    -------
    dict[str, object]
        Return a compact serialisable payload for supervisor audit logs.
    """
    return {
        "obstruction_score": self.obstruction_score,
        "consistency_energy": self.consistency_energy,
        "kernel_dimension": self.kernel_dimension,
        "obstruction_dimension": self.obstruction_dimension,
        "edge_count": self.edge_count,
        "laplacian_shape": list(self.laplacian.shape),
        "residual_shape": list(self.residuals.shape),
        "tolerance": self.tolerance,
        "method": "directed_cellular_sheaf_laplacian",
    }

SheafObstructionSummary dataclass

SheafObstructionSummary(
    severity: str,
    top_residual_edges: tuple[
        tuple[int, int, float, tuple[float, ...]], ...
    ],
    obstruction_score: float,
    warning_threshold: float,
    critical_threshold: float,
)

Review summary for obstruction hardening and audit triage.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable obstruction summary.

Returns

dict[str, object] Return a JSON-serialisable obstruction summary.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable obstruction summary.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable obstruction summary.
    """
    return {
        "severity": self.severity,
        "obstruction_score": self.obstruction_score,
        "warning_threshold": self.warning_threshold,
        "critical_threshold": self.critical_threshold,
        "top_residual_edges": [
            {
                "target": target,
                "source": source,
                "norm": norm,
                "residual": list(residual),
            }
            for target, source, norm, residual in self.top_residual_edges
        ],
    }

SheafControlProposal dataclass

SheafControlProposal(
    baseline_obstruction_score: float,
    projected_obstruction_score: float,
    baseline_consistency_energy: float,
    projected_consistency_energy: float,
    baseline_kernel_dimension: int,
    projected_kernel_dimension: int,
    baseline_obstruction_dimension: int,
    projected_obstruction_dimension: int,
    recommended_update: FloatArray,
    projected_node_states: FloatArray,
    update_norm: float,
    step_size: float,
    max_update_norm: float,
    accepted_for_review: bool,
    non_actuating: bool,
    execution_disabled: bool,
    operator_review_required: bool,
    blocked_reasons: tuple[str, ...],
)

Review-only obstruction-aware sheaf-Laplacian control proposal.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a compact serialisable payload for operator review.

Returns

dict[str, object] Return a compact serialisable payload for operator review.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def to_audit_record(self) -> dict[str, object]:
    """Return a compact serialisable payload for operator review.

    Returns
    -------
    dict[str, object]
        Return a compact serialisable payload for operator review.
    """
    return {
        "method": "sheaf_laplacian_gradient_descent_review",
        "baseline_obstruction_score": self.baseline_obstruction_score,
        "projected_obstruction_score": self.projected_obstruction_score,
        "baseline_consistency_energy": self.baseline_consistency_energy,
        "projected_consistency_energy": self.projected_consistency_energy,
        "cohomology_dimensions": {
            "baseline_kernel_dimension": self.baseline_kernel_dimension,
            "projected_kernel_dimension": self.projected_kernel_dimension,
            "baseline_obstruction_dimension": (self.baseline_obstruction_dimension),
            "projected_obstruction_dimension": (
                self.projected_obstruction_dimension
            ),
        },
        "recommended_update_shape": list(self.recommended_update.shape),
        "projected_node_state_shape": list(self.projected_node_states.shape),
        "update_norm": self.update_norm,
        "step_size": self.step_size,
        "max_update_norm": self.max_update_norm,
        "accepted_for_review": self.accepted_for_review,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "operator_review_required": self.operator_review_required,
        "blocked_reasons": list(self.blocked_reasons),
    }

SheafCoherenceSupervisor

SheafCoherenceSupervisor(tolerance: float = 1e-08)

Assess whether N-channel states agree across restriction maps.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def __init__(self, tolerance: float = 1e-8) -> None:
    self.tolerance = _validate_tolerance(tolerance)
Methods:
assess
assess(
    node_states: FloatArray, restriction_maps: FloatArray
) -> SheafCoherenceResult

Return sheaf obstruction metrics for one supervisor tick.

Parameters

node_states : FloatArray Per-node channel states, shape (N, C). restriction_maps : FloatArray Directed sheaf restriction maps.

Returns

SheafCoherenceResult The sheaf obstruction metrics for the tick.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def assess(
    self,
    node_states: FloatArray,
    restriction_maps: FloatArray,
) -> SheafCoherenceResult:
    """Return sheaf obstruction metrics for one supervisor tick.

    Parameters
    ----------
    node_states : FloatArray
        Per-node channel states, shape ``(N, C)``.
    restriction_maps : FloatArray
        Directed sheaf restriction maps.

    Returns
    -------
    SheafCoherenceResult
        The sheaf obstruction metrics for the tick.
    """
    return sheaf_coherence(
        node_states,
        restriction_maps,
        tolerance=self.tolerance,
    )

Functions:

build_sheaf_obstruction_summary

build_sheaf_obstruction_summary(
    result: SheafCoherenceResult,
    *,
    warning_threshold: float = 0.05,
    critical_threshold: float = 0.25,
    top_k: int = 5,
) -> SheafObstructionSummary

Build a passive triage summary from a sheaf-coherence result.

Parameters

result : SheafCoherenceResult The sheaf-coherence result to summarise. warning_threshold : float Obstruction value above which a warning is raised. critical_threshold : float Coherence threshold below which a child is critical. top_k : int Number of strongest entries to retain.

Returns

SheafObstructionSummary The passive triage summary.

Raises

ValueError If the result or thresholds are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def build_sheaf_obstruction_summary(
    result: SheafCoherenceResult,
    *,
    warning_threshold: float = 0.05,
    critical_threshold: float = 0.25,
    top_k: int = 5,
) -> SheafObstructionSummary:
    """Build a passive triage summary from a sheaf-coherence result.

    Parameters
    ----------
    result : SheafCoherenceResult
        The sheaf-coherence result to summarise.
    warning_threshold : float
        Obstruction value above which a warning is raised.
    critical_threshold : float
        Coherence threshold below which a child is critical.
    top_k : int
        Number of strongest entries to retain.

    Returns
    -------
    SheafObstructionSummary
        The passive triage summary.

    Raises
    ------
    ValueError
        If the result or thresholds are invalid.
    """
    if not isinstance(result, SheafCoherenceResult):
        raise ValueError("result must be a SheafCoherenceResult")
    warn = _validate_tolerance(warning_threshold)
    critical = _validate_tolerance(critical_threshold)
    if critical < warn:
        raise ValueError("critical_threshold must be >= warning_threshold")
    top_k = _validate_top_k(top_k)
    severity = _obstruction_severity(result.obstruction_score, warn, critical)
    return SheafObstructionSummary(
        severity=severity,
        top_residual_edges=_top_residual_edges(result.residuals, top_k),
        obstruction_score=result.obstruction_score,
        warning_threshold=warn,
        critical_threshold=critical,
    )

propose_sheaf_obstruction_control

propose_sheaf_obstruction_control(
    node_states: FloatArray,
    restriction_maps: FloatArray,
    *,
    step_size: float = 0.25,
    max_update_norm: float = 0.25,
    tolerance: float = 1e-08,
    max_backtracking_steps: int = 12,
) -> SheafControlProposal

Propose a review-only correction along the sheaf-Laplacian gradient.

The proposal minimises the cellular-sheaf consistency energy x.T @ L @ x by a bounded explicit gradient step. A small deterministic backtracking line search is used so accepted proposals never increase the measured obstruction energy. The result is an audit artefact only: execution is disabled and any live actuation requires a separate operator approval path.

Parameters

node_states : FloatArray Per-node channel states, shape (N, C). restriction_maps : FloatArray Directed sheaf restriction maps. step_size : float Gradient step size for the proposed correction. max_update_norm : float Maximum norm of the proposed correction. tolerance : float Numerical tolerance. max_backtracking_steps : int Maximum backtracking line-search steps.

Returns

SheafControlProposal The review-only sheaf correction proposal.

Raises

ValueError If the node states or step parameters are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def propose_sheaf_obstruction_control(
    node_states: FloatArray,
    restriction_maps: FloatArray,
    *,
    step_size: float = 0.25,
    max_update_norm: float = 0.25,
    tolerance: float = 1e-8,
    max_backtracking_steps: int = 12,
) -> SheafControlProposal:
    """Propose a review-only correction along the sheaf-Laplacian gradient.

    The proposal minimises the cellular-sheaf consistency energy
    ``x.T @ L @ x`` by a bounded explicit gradient step. A small deterministic
    backtracking line search is used so accepted proposals never increase the
    measured obstruction energy. The result is an audit artefact only:
    execution is disabled and any live actuation requires a separate operator
    approval path.

    Parameters
    ----------
    node_states : FloatArray
        Per-node channel states, shape ``(N, C)``.
    restriction_maps : FloatArray
        Directed sheaf restriction maps.
    step_size : float
        Gradient step size for the proposed correction.
    max_update_norm : float
        Maximum norm of the proposed correction.
    tolerance : float
        Numerical tolerance.
    max_backtracking_steps : int
        Maximum backtracking line-search steps.

    Returns
    -------
    SheafControlProposal
        The review-only sheaf correction proposal.

    Raises
    ------
    ValueError
        If the node states or step parameters are invalid.
    """
    states = _validate_node_states(node_states)
    maps = _validate_restriction_maps(
        restriction_maps,
        (states.shape[0], states.shape[1]),
    )
    step = _validate_positive_step(step_size, "step_size")
    max_norm = _validate_update_norm(max_update_norm)
    tol = _validate_tolerance(tolerance)
    max_steps = _validate_backtracking_steps(max_backtracking_steps)

    baseline = sheaf_coherence(states, maps, tolerance=tol)
    zero_update = np.zeros_like(states, dtype=np.float64)
    if baseline.obstruction_score <= tol or baseline.consistency_energy <= tol:
        return _sheaf_control_proposal(
            baseline=baseline,
            projected=baseline,
            update=zero_update,
            projected_node_states=states,
            step_size=step,
            max_update_norm=max_norm,
            accepted=False,
            blocked_reasons=("no_obstruction_detected",),
        )

    flat_state = states.reshape(-1)
    gradient = (2.0 * baseline.laplacian @ flat_state).reshape(states.shape)

    candidate_projected = baseline
    candidate_update = zero_update
    scale = step
    for _ in range(max_steps):
        update = -scale * gradient
        update_norm = float(np.linalg.norm(update))
        if max_norm == 0.0:
            update = zero_update
        elif update_norm > max_norm:
            update = update * (max_norm / update_norm)
        projected_states = states + update
        projected = sheaf_coherence(projected_states, maps, tolerance=tol)
        if (
            projected.consistency_energy <= baseline.consistency_energy
            and projected.obstruction_score <= baseline.obstruction_score
            and float(np.linalg.norm(update)) > tol
        ):
            return _sheaf_control_proposal(
                baseline=baseline,
                projected=projected,
                update=update,
                projected_node_states=projected_states,
                step_size=scale,
                max_update_norm=max_norm,
                accepted=True,
                blocked_reasons=(),
            )
        candidate_projected = projected
        candidate_update = update
        scale *= 0.5

    return _sheaf_control_proposal(
        baseline=baseline,
        projected=candidate_projected,
        update=candidate_update,
        projected_node_states=states + candidate_update,
        step_size=scale,
        max_update_norm=max_norm,
        accepted=False,
        blocked_reasons=("no_monotone_sheaf_projection",),
    )

sheaf_coherence

sheaf_coherence(
    node_states: FloatArray,
    restriction_maps: FloatArray,
    tolerance: float = 1e-08,
) -> SheafCoherenceResult

Measure cross-channel consistency over a directed cellular sheaf.

Parameters

node_states : FloatArray N-channel node state matrix with shape (n_nodes, n_channels). restriction_maps : FloatArray Directed restriction maps with shape (n_nodes, n_nodes, n_channels, n_channels). Entry restriction_maps[i, j] maps node j into node i. tolerance : float Numerical threshold used for approximate nullity and obstruction counts.

Returns

SheafCoherenceResult A SheafCoherenceResult with the block sheaf Laplacian, directed residual tensor, obstruction score, consistency energy, and audit-visible approximate dimensions.

Raises

ValueError If the node states or restriction maps are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def sheaf_coherence(
    node_states: FloatArray,
    restriction_maps: FloatArray,
    tolerance: float = 1e-8,
) -> SheafCoherenceResult:
    """Measure cross-channel consistency over a directed cellular sheaf.

    Parameters
    ----------
    node_states : FloatArray
        N-channel node state matrix with shape ``(n_nodes, n_channels)``.
    restriction_maps : FloatArray
        Directed restriction maps with shape ``(n_nodes, n_nodes, n_channels,
        n_channels)``. Entry ``restriction_maps[i, j]`` maps node ``j`` into node ``i``.
    tolerance : float
        Numerical threshold used for approximate nullity and obstruction counts.

    Returns
    -------
    SheafCoherenceResult
        A ``SheafCoherenceResult`` with the block sheaf Laplacian, directed residual
        tensor, obstruction score, consistency energy, and audit-visible approximate
        dimensions.

    Raises
    ------
    ValueError
        If the node states or restriction maps are invalid.
    """
    states = _validate_node_states(node_states)
    maps = _validate_restriction_maps(
        restriction_maps,
        (states.shape[0], states.shape[1]),
    )
    tol = _validate_tolerance(tolerance)
    residuals, edge_count = _restriction_residuals(states, maps, tol)
    laplacian = sheaf_laplacian(maps, tol)
    consistency_energy = float(np.sum(residuals * residuals))
    obstruction_dimension = int(
        np.count_nonzero(np.linalg.norm(residuals, axis=2) > tol)
    )
    obstruction_score = (
        0.0 if edge_count == 0 else float(np.sqrt(consistency_energy / edge_count))
    )
    kernel_dimension = _kernel_dimension(laplacian, tol)

    return SheafCoherenceResult(
        laplacian=laplacian,
        residuals=residuals,
        obstruction_score=obstruction_score,
        consistency_energy=consistency_energy,
        kernel_dimension=kernel_dimension,
        obstruction_dimension=obstruction_dimension,
        edge_count=edge_count,
        tolerance=tol,
    )

sheaf_laplacian

sheaf_laplacian(
    restriction_maps: FloatArray, tolerance: float = 1e-08
) -> FloatArray

Build the block sheaf Laplacian from directed restriction maps.

Parameters

restriction_maps : FloatArray Directed sheaf restriction maps. tolerance : float Numerical tolerance.

Returns

FloatArray The block sheaf Laplacian.

Raises

ValueError If the restriction maps are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/sheaf.py
def sheaf_laplacian(
    restriction_maps: FloatArray,
    tolerance: float = 1e-8,
) -> FloatArray:
    """Build the block sheaf Laplacian from directed restriction maps.

    Parameters
    ----------
    restriction_maps : FloatArray
        Directed sheaf restriction maps.
    tolerance : float
        Numerical tolerance.

    Returns
    -------
    FloatArray
        The block sheaf Laplacian.

    Raises
    ------
    ValueError
        If the restriction maps are invalid.
    """
    if _contains_boolean_alias(restriction_maps):
        raise ValueError("restriction_maps must not contain boolean values")
    if _contains_complex_alias(restriction_maps):
        raise ValueError("restriction_maps must not contain complex values")
    try:
        maps = np.asarray(restriction_maps, dtype=np.float64)
    except (TypeError, ValueError) as exc:
        raise ValueError("restriction_maps must be real-valued") from exc
    if (
        maps.ndim != 4
        or maps.shape[0] != maps.shape[1]
        or maps.shape[2] != maps.shape[3]
    ):
        raise ValueError("restriction_maps must have shape (N, N, D, D)")
    if not np.all(np.isfinite(maps)):
        raise ValueError("restriction_maps must be finite")
    tol = _validate_tolerance(tolerance)

    n_nodes, _, n_channels, _ = maps.shape
    dim = n_nodes * n_channels
    laplacian = np.zeros((dim, dim), dtype=np.float64)
    identity: FloatArray = np.eye(n_channels, dtype=np.float64)

    for target in range(n_nodes):
        target_slice = _block_slice(target, n_channels)
        for source in range(n_nodes):
            if target == source:
                continue
            restriction = maps[target, source]
            if not _has_edge(restriction, tol):
                continue
            source_slice = _block_slice(source, n_channels)
            laplacian[target_slice, target_slice] += identity
            laplacian[target_slice, source_slice] -= restriction
            laplacian[source_slice, target_slice] -= restriction.T
            laplacian[source_slice, source_slice] += restriction.T @ restriction

    result: FloatArray = 0.5 * (laplacian + laplacian.T)
    return result

Value-Alignment Guard

ValueAlignmentGuard is a hard safety wrapper around proposed ControlAction lists. It evaluates explicit objective constraints, blocks violating actions, and returns a forced fallback action set when the proposal does not satisfy the configured score threshold.

The guard is intentionally simple and auditable: no hidden reward model is loaded at runtime. Domainpacks can translate their safety or objective priors into ValueConstraint entries and attach the resulting decision record to the normal audit trace.

Policies may also include ValueParetoObjective entries. When present, ValueAlignmentGuard.evaluate(..., objective_deltas={...}) requires finite objective deltas, blocks regressions beyond each objective's allowed tolerance, and requires at least one positive configured objective to improve. Missing objective evidence fails closed and forces the same safe fallback path. Audit records include pareto_violations with the observed delta, required delta, allowed regression, and counterfactual reason.

Binding specs may carry the same policy as a reviewable value_alignment template:

value_alignment:
  minimum_score: 0.8
  constraints:
    - name: limit-coupling
      knob: K
      scope: global
      max_abs_value: 0.1
      weight: 2.0
  fallback_actions:
    - knob: zeta
      scope: global
      value: 0.0
      ttl_s: 1.0
      justification: value guard safe hold
  pareto_objectives:
    - name: safety_margin
      min_delta: 0.01
      max_regression: 0.0

Use value_alignment_policy_from_binding_spec(spec) to convert that template into a ValueAlignmentPolicy. Audit records include hard bound violations, Pareto objective violations, and score-threshold counterfactuals so reviewers can distinguish a blocked unsafe action, a candidate that regresses a protected objective, and a fallback forced by the policy's minimum alignment score.

Domainpack templates now include review-time examples for cardiac rhythm, power grid, network security, fusion equilibrium, neuroscience EEG, brain connectome, sleep architecture, circadian biology, epidemic SIR, and other simulation/replay domainpacks. These templates are guard priors for reviewable candidate actions; they are not live medical, grid, vehicle, financial, public-health, or security operating policies.

from scpn_phase_orchestrator.actuation.mapper import ControlAction
from scpn_phase_orchestrator.supervisor import (
    ValueAlignmentGuard,
    ValueAlignmentPolicy,
    ValueParetoObjective,
    ValueConstraint,
    value_alignment_policy_from_binding_spec,
)

policy = ValueAlignmentPolicy(
    constraints=(ValueConstraint("limit-coupling", knob="K", max_abs_value=0.1),),
    fallback_actions=(
        ControlAction("zeta", "global", 0.0, 1.0, "alignment fallback: hold"),
    ),
    pareto_objectives=(
        ValueParetoObjective("safety_margin", min_delta=0.01, max_regression=0.0),
    ),
)
decision = ValueAlignmentGuard(policy).evaluate(
    proposed_actions,
    objective_deltas={"safety_margin": 0.02},
)
actions_to_apply = decision.actions_to_apply
audit_payload = decision.to_audit_record()

templated_policy = value_alignment_policy_from_binding_spec(binding_spec)

alignment

Pareto-style value guard for supervisor actuation proposals.

Classes

ValueConstraint dataclass

ValueConstraint(
    name: str,
    knob: str = "*",
    scope: str = "*",
    min_value: float | None = None,
    max_value: float | None = None,
    max_abs_value: float | None = None,
    weight: float = 1.0,
)

A hard value constraint over a proposed control action.

knob and scope accept "*" wildcards. Bounds are inclusive. weight controls how much this constraint contributes to the reported alignment score; a failed hard constraint always blocks the action regardless of weight.

Methods:
applies_to
applies_to(action: ControlAction) -> bool

Return whether this constraint applies to action.

Parameters

action : ControlAction The control action to test against the constraints.

Returns

bool True when the constraint applies to the action.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def applies_to(self, action: ControlAction) -> bool:
    """Return whether this constraint applies to ``action``.

    Parameters
    ----------
    action : ControlAction
        The control action to test against the constraints.

    Returns
    -------
    bool
        ``True`` when the constraint applies to the action.
    """
    knob_match = self.knob == "*" or self.knob == action.knob
    scope_match = self.scope == "*" or self.scope == action.scope
    return knob_match and scope_match
violations_for
violations_for(action: ControlAction) -> tuple[str, ...]

Return failed bound names for action.

Parameters

action : ControlAction The control action to test against the constraints.

Returns

tuple[str, ...] The failed bound names for the action.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def violations_for(self, action: ControlAction) -> tuple[str, ...]:
    """Return failed bound names for ``action``.

    Parameters
    ----------
    action : ControlAction
        The control action to test against the constraints.

    Returns
    -------
    tuple[str, ...]
        The failed bound names for the action.
    """
    failures: list[str] = []
    if self.min_value is not None and action.value < self.min_value:
        failures.append("min_value")
    if self.max_value is not None and action.value > self.max_value:
        failures.append("max_value")
    if self.max_abs_value is not None and abs(action.value) > self.max_abs_value:
        failures.append("max_abs_value")
    return tuple(failures)

ValueViolation dataclass

ValueViolation(
    constraint: str,
    knob: str,
    scope: str,
    proposed_value: float,
    failed_bounds: tuple[str, ...],
    counterfactual: str,
)

A blocked action and the value constraint it violated.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable violation record.

Returns

dict[str, object] Return a serialisable violation record.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable violation record.

    Returns
    -------
    dict[str, object]
        Return a serialisable violation record.
    """
    return {
        "constraint": self.constraint,
        "knob": self.knob,
        "scope": self.scope,
        "proposed_value": self.proposed_value,
        "failed_bounds": list(self.failed_bounds),
        "counterfactual": self.counterfactual,
    }

ValueScoreCounterfactual dataclass

ValueScoreCounterfactual(
    observed_score: float,
    required_score: float,
    counterfactual: str,
)

Counterfactual record explaining a score-threshold fallback.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable score-threshold counterfactual.

Returns

dict[str, object] Return a serialisable score-threshold counterfactual.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable score-threshold counterfactual.

    Returns
    -------
    dict[str, object]
        Return a serialisable score-threshold counterfactual.
    """
    return {
        "observed_score": self.observed_score,
        "required_score": self.required_score,
        "counterfactual": self.counterfactual,
    }

ValueParetoObjective dataclass

ValueParetoObjective(
    name: str,
    min_delta: float = 0.0,
    max_regression: float = 0.0,
)

A named objective delta that must stay on the review Pareto frontier.

ValueParetoViolation dataclass

ValueParetoViolation(
    objective: str,
    observed_delta: float | None,
    required_delta: float,
    allowed_regression: float,
    counterfactual: str,
)

A failed Pareto objective review condition.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable Pareto violation record.

Returns

dict[str, object] Return a serialisable Pareto violation record.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable Pareto violation record.

    Returns
    -------
    dict[str, object]
        Return a serialisable Pareto violation record.
    """
    return {
        "objective": self.objective,
        "observed_delta": self.observed_delta,
        "required_delta": self.required_delta,
        "allowed_regression": self.allowed_regression,
        "counterfactual": self.counterfactual,
    }

ValueAlignmentPolicy dataclass

ValueAlignmentPolicy(
    constraints: tuple[ValueConstraint, ...],
    fallback_actions: ActionTuple = (),
    minimum_score: float = 0.0,
    pareto_objectives: tuple[
        ValueParetoObjective, ...
    ] = (),
)

Configured objective constraints and fallback actuation.

ValueAlignmentDecision dataclass

ValueAlignmentDecision(
    approved_actions: ActionTuple,
    blocked_actions: ActionTuple,
    fallback_actions: ActionTuple,
    violations: tuple[ValueViolation, ...],
    score_counterfactuals: tuple[
        ValueScoreCounterfactual, ...
    ],
    pareto_violations: tuple[ValueParetoViolation, ...],
    alignment_score: float,
    minimum_score: float,
)

Result of applying value constraints to proposed actions.

Attributes
satisfied property
satisfied: bool

Return whether the proposed action set passed the guard.

Returns

bool Return whether the proposed action set passed the guard.

actions_to_apply property
actions_to_apply: ActionTuple

Return approved actions or the forced safe fallback path.

Returns

ActionTuple Return approved actions or the forced safe fallback path.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a serialisable guard decision.

Returns

dict[str, object] Return a serialisable guard decision.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def to_audit_record(self) -> dict[str, object]:
    """Return a serialisable guard decision.

    Returns
    -------
    dict[str, object]
        Return a serialisable guard decision.
    """
    return {
        "satisfied": self.satisfied,
        "alignment_score": self.alignment_score,
        "minimum_score": self.minimum_score,
        "approved_count": len(self.approved_actions),
        "blocked_count": len(self.blocked_actions),
        "fallback_count": len(self.fallback_actions),
        "pareto_violation_count": len(self.pareto_violations),
        "violations": [
            violation.to_audit_record() for violation in self.violations
        ],
        "pareto_violations": [
            violation.to_audit_record() for violation in self.pareto_violations
        ],
        "score_counterfactuals": [
            counterfactual.to_audit_record()
            for counterfactual in self.score_counterfactuals
        ],
        "actions_to_apply": [
            _action_record(action) for action in self.actions_to_apply
        ],
    }

ValueAlignmentGuard

ValueAlignmentGuard(policy: ValueAlignmentPolicy)

Block supervisor actions that violate configured value constraints.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def __init__(self, policy: ValueAlignmentPolicy) -> None:
    self.policy = policy
Methods:
evaluate
evaluate(
    actions: list[ControlAction] | ActionTuple,
    *,
    objective_deltas: ObjectiveDeltas | None = None,
) -> ValueAlignmentDecision

Evaluate proposed actions and return an auditable decision.

Parameters

actions : list[ControlAction] | ActionTuple The control actions to apply or assess. objective_deltas : ObjectiveDeltas | None Per-objective deltas for the proposed actions, or None.

Returns

ValueAlignmentDecision The auditable value-alignment decision.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def evaluate(
    self,
    actions: list[ControlAction] | ActionTuple,
    *,
    objective_deltas: ObjectiveDeltas | None = None,
) -> ValueAlignmentDecision:
    """Evaluate proposed actions and return an auditable decision.

    Parameters
    ----------
    actions : list[ControlAction] | ActionTuple
        The control actions to apply or assess.
    objective_deltas : ObjectiveDeltas | None
        Per-objective deltas for the proposed actions, or ``None``.

    Returns
    -------
    ValueAlignmentDecision
        The auditable value-alignment decision.
    """
    proposed = tuple(actions)
    approved: list[ControlAction] = []
    blocked: list[ControlAction] = []
    violations: list[ValueViolation] = []
    scores: list[float] = []

    for action in proposed:
        action_violations = self._violations_for_action(action)
        if action_violations:
            blocked.append(action)
            violations.extend(action_violations)
            scores.append(0.0)
        else:
            approved.append(action)
            scores.append(self._score_action(action))

    alignment_score = 1.0 if not scores else float(min(scores))
    pareto_violations = _pareto_violations(
        self.policy.pareto_objectives,
        objective_deltas,
    )
    return ValueAlignmentDecision(
        approved_actions=tuple(approved),
        blocked_actions=tuple(blocked),
        fallback_actions=self.policy.fallback_actions,
        violations=tuple(violations),
        pareto_violations=pareto_violations,
        score_counterfactuals=_score_counterfactuals(
            alignment_score,
            self.policy.minimum_score,
            violations,
        ),
        alignment_score=alignment_score,
        minimum_score=self.policy.minimum_score,
    )

Functions:

calibrate_value_alignment_replay_evidence

calibrate_value_alignment_replay_evidence(
    policy: ValueAlignmentPolicy,
    replay_cases: Mapping[
        str, list[ControlAction] | ActionTuple
    ],
    *,
    evidence_label: str = "value_alignment_replay_calibration",
) -> dict[str, object]

Calibrate a value-alignment policy against replayed action proposals.

The returned artifact is deterministic and review-only: it records what the guard would approve, block, or divert to fallback on replayed cases, but it never authorises live actuation. This gives production reviewers evidence for guard behaviour before any deployment-tier enforcement is claimed.

Parameters

policy : ValueAlignmentPolicy The value-alignment policy to calibrate. replay_cases : Mapping[str, list[ControlAction] | ActionTuple] Replay action proposals keyed by case name. evidence_label : str Label recorded with the calibration evidence.

Returns

dict[str, object] The calibration evidence for the policy.

Raises

ValueError If the policy or replay cases are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def calibrate_value_alignment_replay_evidence(
    policy: ValueAlignmentPolicy,
    replay_cases: Mapping[str, list[ControlAction] | ActionTuple],
    *,
    evidence_label: str = "value_alignment_replay_calibration",
) -> dict[str, object]:
    """Calibrate a value-alignment policy against replayed action proposals.

    The returned artifact is deterministic and review-only: it records what the
    guard would approve, block, or divert to fallback on replayed cases, but it
    never authorises live actuation. This gives production reviewers evidence
    for guard behaviour before any deployment-tier enforcement is claimed.

    Parameters
    ----------
    policy : ValueAlignmentPolicy
        The value-alignment policy to calibrate.
    replay_cases : Mapping[str, list[ControlAction] | ActionTuple]
        Replay action proposals keyed by case name.
    evidence_label : str
        Label recorded with the calibration evidence.

    Returns
    -------
    dict[str, object]
        The calibration evidence for the policy.

    Raises
    ------
    ValueError
        If the policy or replay cases are invalid.
    """
    if not isinstance(policy, ValueAlignmentPolicy):
        raise ValueError("policy must be a ValueAlignmentPolicy")
    if not replay_cases:
        raise ValueError(
            "value-alignment calibration requires at least one replay case"
        )
    if not isinstance(evidence_label, str) or not evidence_label:
        raise ValueError("evidence_label must be a non-empty string")

    guard = ValueAlignmentGuard(policy)
    records: list[dict[str, object]] = []
    approved_case_count = 0
    blocked_case_count = 0
    threshold_fallback_case_count = 0
    fallback_applied_case_count = 0

    for case_id, actions in replay_cases.items():
        if not isinstance(case_id, str) or not case_id:
            raise ValueError(
                "value-alignment replay case id must be a non-empty string"
            )
        proposed_actions = _validated_replay_actions(actions, case_id)
        decision = guard.evaluate(proposed_actions)
        audit_record = decision.to_audit_record()

        if decision.satisfied:
            approved_case_count += 1
        if decision.violations:
            blocked_case_count += 1
        if decision.score_counterfactuals:
            threshold_fallback_case_count += 1
        if not decision.satisfied and decision.fallback_actions:
            fallback_applied_case_count += 1

        records.append(
            {
                "case_id": case_id,
                "proposed_action_count": len(proposed_actions),
                "satisfied": decision.satisfied,
                "alignment_score": decision.alignment_score,
                "minimum_score": decision.minimum_score,
                "approved_count": len(decision.approved_actions),
                "blocked_count": len(decision.blocked_actions),
                "fallback_count": len(decision.fallback_actions),
                "violation_count": len(decision.violations),
                "score_counterfactual_count": len(decision.score_counterfactuals),
                "actions_to_apply": audit_record["actions_to_apply"],
                "violations": audit_record["violations"],
                "score_counterfactuals": audit_record["score_counterfactuals"],
            }
        )

    artifact: dict[str, object] = {
        "schema": "scpn_value_alignment_replay_calibration_v1",
        "evidence_label": evidence_label,
        "replay_case_count": len(records),
        "approved_case_count": approved_case_count,
        "blocked_case_count": blocked_case_count,
        "threshold_fallback_case_count": threshold_fallback_case_count,
        "fallback_applied_case_count": fallback_applied_case_count,
        "calibration_actuation_permitted": False,
        "decision_records": records,
    }
    canonical = json.dumps(artifact, sort_keys=True, separators=(",", ":"))
    artifact["calibration_sha256"] = sha256(canonical.encode("utf-8")).hexdigest()
    return artifact

value_alignment_policy_from_binding_spec

value_alignment_policy_from_binding_spec(
    spec: object,
) -> ValueAlignmentPolicy | None

Build a policy from BindingSpec.value_alignment when present.

Parameters

spec : object The binding spec to read value-alignment configuration from.

Returns

ValueAlignmentPolicy | None The value-alignment policy, or None if not present.

Raises

ValueError If the value-alignment configuration is malformed.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def value_alignment_policy_from_binding_spec(
    spec: object,
) -> ValueAlignmentPolicy | None:
    """Build a policy from ``BindingSpec.value_alignment`` when present.

    Parameters
    ----------
    spec : object
        The binding spec to read value-alignment configuration from.

    Returns
    -------
    ValueAlignmentPolicy | None
        The value-alignment policy, or ``None`` if not present.

    Raises
    ------
    ValueError
        If the value-alignment configuration is malformed.
    """
    template = getattr(spec, "value_alignment", None)
    if not template:
        return None
    if not isinstance(template, Mapping):
        raise ValueError("binding spec value_alignment must be a mapping")
    return value_alignment_policy_from_template(template)

value_alignment_policy_from_template

value_alignment_policy_from_template(
    template: Mapping[str, object],
) -> ValueAlignmentPolicy

Build a value-alignment policy from a binding-spec template mapping.

Expected shape::

value_alignment:
  minimum_score: 0.8
  constraints:
    - name: limit-coupling
      knob: K
      max_abs_value: 0.1
  fallback_actions:
    - knob: zeta
      scope: global
      value: 0.0
      ttl_s: 1.0
      justification: safe hold
Parameters

template : Mapping[str, object] The value-alignment template mapping.

Returns

ValueAlignmentPolicy The value-alignment policy built from the template.

Source code in src/scpn_phase_orchestrator/supervisor/alignment.py
def value_alignment_policy_from_template(
    template: Mapping[str, object],
) -> ValueAlignmentPolicy:
    """Build a value-alignment policy from a binding-spec template mapping.

    Expected shape::

        value_alignment:
          minimum_score: 0.8
          constraints:
            - name: limit-coupling
              knob: K
              max_abs_value: 0.1
          fallback_actions:
            - knob: zeta
              scope: global
              value: 0.0
              ttl_s: 1.0
              justification: safe hold

    Parameters
    ----------
    template : Mapping[str, object]
        The value-alignment template mapping.

    Returns
    -------
    ValueAlignmentPolicy
        The value-alignment policy built from the template.
    """
    constraints = tuple(
        _constraint_from_template(item, index)
        for index, item in enumerate(_template_list(template, "constraints"))
    )
    fallback_actions = tuple(
        _action_from_template(item, index)
        for index, item in enumerate(_template_list(template, "fallback_actions"))
    )
    pareto_objectives = tuple(
        _pareto_objective_from_template(item, index)
        for index, item in enumerate(_template_list(template, "pareto_objectives"))
    )
    minimum_score = _template_float(template.get("minimum_score", 0.0))
    return ValueAlignmentPolicy(
        constraints=constraints,
        fallback_actions=fallback_actions,
        minimum_score=minimum_score,
        pareto_objectives=pareto_objectives,
    )

Policy Engine

Rule-based evaluation of supervisor actions.

SupervisorPolicy

SupervisorPolicy(
    regime_manager: RegimeManager,
    petri_adapter: PetriNetAdapter | None = None,
    gains: SupervisorPolicyGains | None = None,
    admission_gate: PolicyCBFAdmissionGate | None = None,
)

decide()

def decide(
    upde_state: UPDEState,
    boundary_state: BoundaryState,
    petri_ctx: dict[str, float] | None = None,
) -> list[ControlAction]

Returns a list of ControlAction instructions. Each action specifies:

Field Type Example
knob str "K", "zeta", "psi"
scope str "global", "layer_0"
value float 0.05 (K boost), 0.1 (zeta damp)
ttl_s float 5.0 (action expires after 5s)
justification str "degraded: K boost"

When an optional PolicyCBFAdmissionGate is supplied, matching supervisor actions are admitted through verified neural CBF filters before decide() returns. last_admission_records exposes deterministic audit records for the latest call, including the CBF filter digest, certificate digest, admission status, admitted value, and SMT-LIB artefact hash.

Regime-action mapping

Regime Actions
NOMINAL None (no intervention)
DEGRADED K boost +0.05 (global)
CRITICAL ζ damping +0.1 + K reduce -0.03 (worst layer)
RECOVERY K restore +0.025 (half boost, global)

Hard violation override

Hard boundary violations (BoundaryState.hard_violations) force CRITICAL regardless of R values.

Policy CBF Admission

PolicyCBFAdmissionGate is the opt-in bridge between heuristic supervisor proposals and certificate-bound neural CBF admission. Each PolicyCBFChannel selects one action knob/scope, validates a matching BarrierCertificate for the provided ControlBarrierFilter, extracts named runtime metrics from UPDEState and BoundaryState, and emits a deterministic SMT-LIB admission artefact for the scalar CBF half-space checked at that decision. The gate does not execute Z3 locally and does not actuate; it constrains, admits, or rejects proposal values before downstream projection.

cbf_admission

Certificate-bound CBF admission for supervisor ControlAction proposals.

SupervisorPolicy emits bounded, non-actuating action proposals. This module adds an optional admission layer for deployments that have a verified neural Control Barrier Function (CBF): matching actions are passed through the existing certificate-bound CBF governor, and every decision emits a deterministic SMT-LIB admission artefact. The artefact captures the exact scalar CBF half-space, control bounds, selected action, and filter/certificate digests; it does not run Z3 locally and does not grant actuation.

Classes

PolicyCBFAdmissionRecord dataclass

PolicyCBFAdmissionRecord(
    knob: str,
    scope: str,
    proposed_value: float,
    admitted_value: float,
    status: str,
    stages_applied: tuple[str, ...],
    violations: tuple[str, ...],
    barrier_value: float | None,
    filter_digest: str,
    certificate_verification_digest: str,
    smt_artifact: FormalTextArtifact,
    smt_artifact_hash: str,
)

Audit record for one CBF-admitted supervisor action.

Attributes

knob, scope : str Action channel admitted by the CBF gate. proposed_value : float Original supervisor proposal. admitted_value : float Value admitted by the CBF governor. status : str Governor status: admitted, constrained, or rejected. stages_applied : tuple[str, ...] Envelope stages that modified the proposal. violations : tuple[str, ...] Rejection reasons, if any. barrier_value : float | None Current CBF value h(x). filter_digest : str Digest of the CBF filter configuration. certificate_verification_digest : str Digest of the certificate envelope used to validate the filter. smt_artifact : FormalTextArtifact Deterministic SMT-LIB admission artefact for this decision. smt_artifact_hash : str SHA-256 hash of :attr:smt_artifact. content_hash : str SHA-256 hash of the audit record excluding the SMT text.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe CBF admission audit record.

Returns

dict[str, object] Admission decision, barrier/certificate digests, SMT artefact hash, and deterministic content hash.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe CBF admission audit record.

    Returns
    -------
    dict[str, object]
        Admission decision, barrier/certificate digests, SMT artefact hash,
        and deterministic content hash.
    """
    record = self._payload()
    record["content_hash"] = self.content_hash
    return record

PolicyCBFAdmissionResult dataclass

PolicyCBFAdmissionResult(
    actions: tuple[ControlAction, ...],
    records: tuple[PolicyCBFAdmissionRecord, ...],
)

CBF admission output for a batch of supervisor actions.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe batch admission record.

Returns

dict[str, object] Admitted action count plus per-action CBF admission records.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe batch admission record.

    Returns
    -------
    dict[str, object]
        Admitted action count plus per-action CBF admission records.
    """
    return {
        "actions": len(self.actions),
        "records": [record.to_audit_record() for record in self.records],
    }

PolicyCBFChannel dataclass

PolicyCBFChannel(
    knob: str,
    scope: str,
    barrier_filter: ControlBarrierFilter,
    barrier_certificate: BarrierCertificate,
    state_metrics: tuple[str, ...],
    drift_bounds: tuple[float, ...],
    previous_action: float = 0.0,
    max_rate: float | None = None,
)

Certificate-bound CBF admission channel for one action knob/scope.

Parameters

knob, scope : str Action selector. Only exact (knob, scope) matches are admitted by this channel. barrier_filter : ControlBarrierFilter Verified CBF filter for the scalar action value. barrier_certificate : BarrierCertificate Certificate that validates :attr:barrier_filter. state_metrics : tuple[str, ...] Names of UPDE/boundary metrics used as the CBF state vector. drift_bounds : tuple[float, ...] Deterministic drift vector supplied to the CBF filter for admission. previous_action : float Held fallback and rate-limit reference for rejected decisions. max_rate : float | None Optional per-call rate limit. None uses the full control span.

Methods:
matches
matches(action: ControlAction) -> bool

Return whether action belongs to this CBF channel.

Parameters

action : ControlAction Supervisor action proposal to compare with this channel's selector.

Returns

bool True when both knob and scope match exactly.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def matches(self, action: ControlAction) -> bool:
    """Return whether ``action`` belongs to this CBF channel.

    Parameters
    ----------
    action : ControlAction
        Supervisor action proposal to compare with this channel's selector.

    Returns
    -------
    bool
        ``True`` when both knob and scope match exactly.
    """
    return action.knob == self.knob and action.scope == self.scope
admit
admit(
    action: ControlAction,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> tuple[ControlAction, PolicyCBFAdmissionRecord]

Admit one matching action through the verified CBF governor.

Parameters

action : ControlAction Supervisor action proposal. It must match :meth:matches. upde_state : UPDEState Current UPDE metrics used to build the CBF state vector. boundary_state : BoundaryState Current boundary metrics used to build the CBF state vector.

Returns

tuple[ControlAction, PolicyCBFAdmissionRecord] The admitted action and its deterministic audit record.

Raises

ValueError If action does not match this channel.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def admit(
    self,
    action: ControlAction,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> tuple[ControlAction, PolicyCBFAdmissionRecord]:
    """Admit one matching action through the verified CBF governor.

    Parameters
    ----------
    action : ControlAction
        Supervisor action proposal. It must match :meth:`matches`.
    upde_state : UPDEState
        Current UPDE metrics used to build the CBF state vector.
    boundary_state : BoundaryState
        Current boundary metrics used to build the CBF state vector.

    Returns
    -------
    tuple[ControlAction, PolicyCBFAdmissionRecord]
        The admitted action and its deterministic audit record.

    Raises
    ------
    ValueError
        If ``action`` does not match this channel.
    """
    if not self.matches(action):
        raise ValueError("action does not match this CBF channel")
    state = _state_vector(self.state_metrics, upde_state, boundary_state)
    drift = _finite_vector(self.drift_bounds, "drift_bounds")
    governor = FoundationModelGovernor(
        control_lo=self.barrier_filter.control_lo,
        control_hi=self.barrier_filter.control_hi,
        max_rate=self._max_rate(),
        barrier_filter=self.barrier_filter,
        barrier_certificate=self.barrier_certificate,
    )
    decision = governor.govern(
        action.value,
        state,
        drift,
        previous_action=self.previous_action,
    )
    smt_artifact = _admission_smt(
        channel=self,
        action=action,
        state=state,
        drift=drift,
        admitted_value=decision.admitted_action,
    )
    smt_hash = _sha256_text(smt_artifact.text)
    record = PolicyCBFAdmissionRecord(
        knob=action.knob,
        scope=action.scope,
        proposed_value=action.value,
        admitted_value=decision.admitted_action,
        status=decision.status,
        stages_applied=decision.stages_applied,
        violations=decision.violations,
        barrier_value=decision.barrier_value,
        filter_digest=self.barrier_filter.filter_digest,
        certificate_verification_digest=(
            self.barrier_certificate.verification_digest
        ),
        smt_artifact=smt_artifact,
        smt_artifact_hash=smt_hash,
    )
    return _action_from_decision(action, record), record

PolicyCBFAdmissionGate

PolicyCBFAdmissionGate(
    channels: Sequence[PolicyCBFChannel],
)

Apply configured CBF channels to supervisor action proposals.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def __init__(self, channels: Sequence[PolicyCBFChannel]) -> None:
    if not channels:
        raise ValueError("channels must contain at least one PolicyCBFChannel")
    if not all(isinstance(channel, PolicyCBFChannel) for channel in channels):
        raise ValueError("channels must contain only PolicyCBFChannel objects")
    keys = [(channel.knob, channel.scope) for channel in channels]
    if len(set(keys)) != len(keys):
        raise ValueError("CBF admission channels must be unique by knob/scope")
    self._channels = tuple(channels)
Methods:
admit_actions
admit_actions(
    actions: Sequence[ControlAction],
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> PolicyCBFAdmissionResult

Admit matching actions and return transformed actions plus records.

Parameters

actions : Sequence[ControlAction] Supervisor action proposals. upde_state : UPDEState Current UPDE metrics. boundary_state : BoundaryState Current boundary-observer metrics.

Returns

PolicyCBFAdmissionResult Admitted action tuple and CBF audit records for matched actions.

Source code in src/scpn_phase_orchestrator/supervisor/cbf_admission.py
def admit_actions(
    self,
    actions: Sequence[ControlAction],
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> PolicyCBFAdmissionResult:
    """Admit matching actions and return transformed actions plus records.

    Parameters
    ----------
    actions : Sequence[ControlAction]
        Supervisor action proposals.
    upde_state : UPDEState
        Current UPDE metrics.
    boundary_state : BoundaryState
        Current boundary-observer metrics.

    Returns
    -------
    PolicyCBFAdmissionResult
        Admitted action tuple and CBF audit records for matched actions.
    """
    admitted: list[ControlAction] = []
    records: list[PolicyCBFAdmissionRecord] = []
    for action in actions:
        channel = self._matching_channel(action)
        if channel is None:
            admitted.append(action)
            continue
        admitted_action, record = channel.admit(action, upde_state, boundary_state)
        admitted.append(admitted_action)
        records.append(record)
    return PolicyCBFAdmissionResult(tuple(admitted), tuple(records))

Performance: decide() < 50 μs.

policy

Reactive supervisor policy that maps regimes and state into control proposals.

SupervisorPolicy derives a proposed regime from direct metrics or an optional Petri adapter, commits it through RegimeManager, and emits bounded ControlAction proposals for degraded, critical, or recovery states. Petri failures fall back to direct regime logic. The policy only proposes actions; it does not apply actuation or mutate coupling matrices.

Classes

SupervisorPolicyGains dataclass

SupervisorPolicyGains(
    k_bump: float = 0.05,
    zeta_bump: float = 0.1,
    k_reduce: float = -0.03,
    restore_fraction: float = 0.5,
)

Tunable regime-action gains for a deployment-specific supervisor.

SupervisorPolicy

SupervisorPolicy(
    regime_manager: RegimeManager,
    petri_adapter: PetriNetAdapter | None = None,
    gains: SupervisorPolicyGains | None = None,
    admission_gate: PolicyCBFAdmissionGate | None = None,
)

Decide control actions based on regime and system state.

When petri_adapter is provided, regime is derived from the Petri net marking instead of RegimeManager.evaluate().

Source code in src/scpn_phase_orchestrator/supervisor/policy.py
def __init__(
    self,
    regime_manager: RegimeManager,
    petri_adapter: PetriNetAdapter | None = None,
    gains: SupervisorPolicyGains | None = None,
    admission_gate: PolicyCBFAdmissionGate | None = None,
) -> None:
    self._regime_manager = regime_manager
    self._petri_adapter = petri_adapter
    self._gains = gains or SupervisorPolicyGains()
    self._admission_gate = admission_gate
    self._last_admission_records: tuple[PolicyCBFAdmissionRecord, ...] = ()
Attributes
last_admission_records property
last_admission_records: tuple[PolicyCBFAdmissionRecord, ...]

Return the CBF admission records from the latest decision.

Returns

tuple[PolicyCBFAdmissionRecord, ...] Deterministic audit records for actions matched by the optional CBF admission gate in the previous :meth:decide call.

Methods:
decide
decide(
    upde_state: UPDEState,
    boundary_state: BoundaryState,
    petri_ctx: dict[str, float] | None = None,
) -> list[ControlAction]

Evaluate regime and return control actions for the current state.

Parameters

upde_state : UPDEState The current UPDE state. boundary_state : BoundaryState The current boundary-observer state. petri_ctx : dict[str, float] | None Petri context metric values, or None.

Returns

list[ControlAction] The control actions proposed for the current state.

Source code in src/scpn_phase_orchestrator/supervisor/policy.py
def decide(
    self,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
    petri_ctx: dict[str, float] | None = None,
) -> list[ControlAction]:
    """Evaluate regime and return control actions for the current state.

    Parameters
    ----------
    upde_state : UPDEState
        The current UPDE state.
    boundary_state : BoundaryState
        The current boundary-observer state.
    petri_ctx : dict[str, float] | None
        Petri context metric values, or ``None``.

    Returns
    -------
    list[ControlAction]
        The control actions proposed for the current state.
    """
    proposed = self._proposed_regime(upde_state, boundary_state, petri_ctx)
    regime = self._regime_manager.transition(proposed)

    actions = self._actions_for_regime(regime, upde_state)
    if self._admission_gate is None:
        self._last_admission_records = ()
    else:
        admitted = self._admission_gate.admit_actions(
            actions, upde_state, boundary_state
        )
        actions = list(admitted.actions)
        self._last_admission_records = admitted.records
    logger.info(
        "supervisor decide: regime=%s actions=%d",
        regime.value,
        len(actions),
        extra={
            "regime": regime.value,
            "n_actions": len(actions),
            "n_violations": len(boundary_state.violations),
            "stability_proxy": upde_state.stability_proxy,
            "knobs": [a.knob for a in actions],
        },
    )
    return actions

Causal Counterfactual Rollouts

CausalInterventionEngine evaluates proposed supervisor actions by running paired UPDE trajectories from the same state:

  • baseline: no action
  • intervention: action-adjusted K, alpha, zeta, or Psi

The result is a CounterfactualRollout with R and Psi trajectories, final and mean R deltas, signed final phase delta, and a serialisable audit payload. Counterfactual phases, frequency vectors, coupling matrices, phase-lag matrices, and lagged causal traces are validated as finite real-valued numeric arrays before simulation or causal scoring. Boolean aliases and complex/object-complex payloads are rejected before float coercion so rollouts and lagged-linear influence estimates stay on the real Kuramoto state space.

from scpn_phase_orchestrator.supervisor import CausalInterventionEngine

engine = CausalInterventionEngine(n_oscillators=8, dt=0.01, horizon=20)
rollout = engine.evaluate_actions(phases, omegas, knm, alpha, 0.0, 0.0, actions)
record = rollout.to_audit_record()
attribution = rollout.attribute(threshold=1e-3).to_audit_record()

This is the first causal-supervision slice: it does not claim formal do-calculus yet, but it makes every proposed actuation comparable against a no-action counterfactual under the same UPDE dynamics.

CounterfactualRollout.attribute() compresses the final and mean R deltas into an audit-ready effect label: stabilising, neutral, or destabilising.

learn_causal_graph() adds a lightweight live causal-model learner. It estimates signed directed edges from lagged monitor traces and appends explicit do(knob:scope) -> R edges from paired counterfactual rollouts. The output is a CausalGraphEstimate with JSON-safe nodes, edge weights, confidence scores, lags, and evidence labels for the audit trail.

from scpn_phase_orchestrator.supervisor import learn_causal_graph

graph = learn_causal_graph(
    {"R_good": good_trace, "R_bad": bad_trace},
    [rollout],
    lag=1,
    min_abs_weight=1e-4,
)
audit_graph = graph.to_audit_record()

build_temporal_causal_hypergraph_experiment() is the research-screening layer for temporal-causal hypergraph candidates. It compares each proposed time-symmetric hyperedge against a deterministic family of conventional baselines before any claim can be made:

  • lagged-linear graph edge score from learn_causal_graph();
  • lagged Pearson correlation between source and future target;
  • lagged-delta Pearson correlation between source and target increment;
  • pairwise Granger-style residual improvement over target history;
  • target-persistence null correlation.

Candidate hyperedges are accepted for review only when their score beats the strongest baseline by the configured margin. The manifest stays research-only: production claims, hot patches, and actuation are disabled, and non-winning candidates are retained as blocked evidence for audit comparison. Use this for offline discovery of higher-order temporal coupling hypotheses, not for real-time causal intervention.

from scpn_phase_orchestrator.supervisor import (
    build_temporal_causal_hypergraph_experiment,
)

manifest = build_temporal_causal_hypergraph_experiment(
    {
        "driver": driver_trace,
        "response": response_trace,
        "distractor": distractor_trace,
    },
    [
        {
            "sources": ["driver", "response"],
            "target": "response",
            "time_offsets": [-1, 0],
            "score": candidate_score,
        }
    ],
    lag=1,
    min_abs_weight=1e-4,
    required_baseline_margin=0.1,
)
assert manifest["production_claim_permitted"] is False
assert manifest["baseline"]["strongest_baseline"] in {
    "lagged_linear_graph",
    "lagged_pearson",
    "lagged_delta_pearson",
    "granger_residual_improvement",
    "target_persistence_null",
}

Domainpack demos:

  • domainpacks/cardiac_rhythm/causal_attribution_demo.py evaluates a pacing-drive candidate against a ventricular-disturbance baseline.
  • domainpacks/power_grid/causal_attribution_demo.py evaluates a governor droop coupling candidate against a no-action load-step baseline.
  • domainpacks/traffic_flow/causal_attribution_demo.py evaluates a signal-cycle coupling candidate against a no-action corridor-spillback baseline.
  • domainpacks/network_security/causal_attribution_demo.py evaluates a firewall-coupling candidate against a no-action lateral-movement baseline.

Backend and cost: each evaluation performs two UPDE rollouts over the configured horizon, so work scales with 2 * horizon engine steps. It uses the existing UPDEEngine backend dispatcher; Rust acceleration is used when available, otherwise the NumPy path is used.

causal

Causal graph learning and counterfactual supervisor rollout diagnostics.

The module estimates directed influence from traces and compares baseline UPDE trajectories against parameter-intervention rollouts derived from proposed control actions. Inputs are validated for finite dimensions before simulation; action application mutates local copies of coupling and phase-lag matrices only. Outputs are audit-ready records and attribution summaries, not live actuation.

Classes

CausalAttribution dataclass

CausalAttribution(
    effect: str,
    trajectory_consistency: float,
    score: float,
    delta_R_final: float,
    delta_R_mean: float,
    threshold: float,
)

Attribution summary derived from a paired counterfactual rollout.

trajectory_consistency reports the fraction of the rollout horizon over which the per-step order-parameter delta holds the attributed effect's sign (or, for a neutral verdict, stays within the neutral band |delta| <= threshold). It is a deterministic property of the single paired trajectory — how steadily the intervention pushes R in the attributed direction — not a statistical or frequentist confidence: the rollout draws no samples, so there is no sampling distribution and no p-value to report. A value near 1.0 means the effect is steady across the whole horizon; a lower value means the sign only settles late or oscillates. Effect magnitude lives in score/delta_R_final/delta_R_mean, kept separate so a steady-but-small effect is not confused with a large-but-transient one.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable attribution payload.

Returns

dict[str, object] Return a JSON-serialisable attribution payload.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable attribution payload.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable attribution payload.
    """
    return {
        "effect": self.effect,
        "trajectory_consistency": self.trajectory_consistency,
        "score": self.score,
        "delta_R_final": self.delta_R_final,
        "delta_R_mean": self.delta_R_mean,
        "threshold": self.threshold,
    }

CausalInfluenceEdge dataclass

CausalInfluenceEdge(
    source: str,
    target: str,
    weight: float,
    confidence: float,
    lag: int,
    evidence: str,
)

Signed directed influence estimate between live causal graph nodes.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable causal-edge payload.

Returns

dict[str, object] Return a JSON-serialisable causal-edge payload.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable causal-edge payload.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable causal-edge payload.
    """
    return {
        "source": self.source,
        "target": self.target,
        "weight": self.weight,
        "confidence": self.confidence,
        "lag": self.lag,
        "evidence": self.evidence,
    }

CausalGraphEstimate dataclass

CausalGraphEstimate(
    nodes: tuple[str, ...],
    edges: tuple[CausalInfluenceEdge, ...],
    lag: int,
    min_abs_weight: float,
)

Audit-ready directed causal graph learned from traces and interventions.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable causal graph estimate.

Returns

dict[str, object] Return a JSON-serialisable causal graph estimate.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable causal graph estimate.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable causal graph estimate.
    """
    return {
        "nodes": list(self.nodes),
        "edges": [edge.to_audit_record() for edge in self.edges],
        "lag": self.lag,
        "min_abs_weight": self.min_abs_weight,
    }

InterventionParameters dataclass

InterventionParameters(
    knm: FloatArray,
    alpha: FloatArray,
    zeta: float,
    psi: float,
)

UPDE parameters after applying a supervisor intervention.

CounterfactualRollout dataclass

CounterfactualRollout(
    baseline_R: list[float],
    intervention_R: list[float],
    baseline_psi: list[float],
    intervention_psi: list[float],
    delta_R_final: float,
    delta_R_mean: float,
    delta_psi_final: float,
    actions: tuple[ControlAction, ...],
)

Paired baseline/intervention rollout summary for audit logging.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-serialisable counterfactual audit payload.

Returns

dict[str, object] Return a JSON-serialisable counterfactual audit payload.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-serialisable counterfactual audit payload.

    Returns
    -------
    dict[str, object]
        Return a JSON-serialisable counterfactual audit payload.
    """
    return {
        "baseline_R": self.baseline_R,
        "intervention_R": self.intervention_R,
        "baseline_psi": self.baseline_psi,
        "intervention_psi": self.intervention_psi,
        "delta_R_final": self.delta_R_final,
        "delta_R_mean": self.delta_R_mean,
        "delta_psi_final": self.delta_psi_final,
        "actions": [
            {
                "knob": action.knob,
                "scope": action.scope,
                "value": action.value,
                "ttl_s": action.ttl_s,
                "justification": action.justification,
            }
            for action in self.actions
        ],
    }
attribute
attribute(threshold: float = 0.001) -> CausalAttribution

Summarise whether the intervention caused a measurable R change.

The verdict is decided by the trajectory-averaged effect score; its steadiness across the horizon is reported separately as trajectory_consistency (see :class:CausalAttribution), an honest deterministic measure rather than a statistical confidence.

Parameters

threshold : float Decision threshold.

Returns

CausalAttribution The causal attribution of the intervention.

Raises

ValueError If threshold is invalid.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def attribute(self, threshold: float = 1e-3) -> CausalAttribution:
    """Summarise whether the intervention caused a measurable R change.

    The verdict is decided by the trajectory-averaged effect ``score``; its
    steadiness across the horizon is reported separately as
    ``trajectory_consistency`` (see :class:`CausalAttribution`), an honest
    deterministic measure rather than a statistical confidence.

    Parameters
    ----------
    threshold : float
        Decision threshold.

    Returns
    -------
    CausalAttribution
        The causal attribution of the intervention.

    Raises
    ------
    ValueError
        If ``threshold`` is invalid.
    """
    if not np.isfinite(threshold) or threshold < 0.0:
        raise ValueError("threshold must be finite and non-negative")
    score = 0.5 * (self.delta_R_final + self.delta_R_mean)
    magnitude = abs(score)
    if magnitude <= threshold:
        effect = "neutral"
    elif score > 0.0:
        effect = "stabilising"
    else:
        effect = "destabilising"
    return CausalAttribution(
        effect=effect,
        trajectory_consistency=self._trajectory_consistency(
            score, effect, threshold
        ),
        score=float(score),
        delta_R_final=self.delta_R_final,
        delta_R_mean=self.delta_R_mean,
        threshold=threshold,
    )

CausalInterventionEngine

CausalInterventionEngine(
    n_oscillators: int,
    dt: float,
    horizon: int = 20,
    method: str = "rk4",
    *,
    layer_membership: Mapping[str, Sequence[int]]
    | None = None,
)

Counterfactual UPDE rollouts for supervisor actions.

The engine answers the first causal supervision question: from the same state, what would the order-parameter trajectory look like with and without the proposed intervention?

Parameters

n_oscillators : int Number of oscillators in the network. dt : float Integration timestep. horizon : int Number of rollout steps. method : str UPDE integration method. layer_membership : Mapping[str, Sequence[int]] or None Optional named layers with their member oscillator indices, enabling do(K, layer_<name>) interventions. "layer_<name>" (default) or "layer_<name>.within" perturbs the within-layer coupling sub-block; "layer_<name>.incident" perturbs every coupling incident to a layer member (the set generalisation of oscillator_). Without it, any layer-scoped action is rejected.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def __init__(
    self,
    n_oscillators: int,
    dt: float,
    horizon: int = 20,
    method: str = "rk4",
    *,
    layer_membership: Mapping[str, Sequence[int]] | None = None,
):
    self._n = _require_positive_int(
        n_oscillators,
        type_message="n_oscillators must be an integer",
        range_message="n_oscillators must be >= 1",
    )
    self._dt = _require_positive_real(
        dt,
        message="dt must be finite and > 0",
    )
    self._horizon = _require_positive_int(
        horizon,
        type_message="horizon must be a positive integer",
        range_message="horizon must be >= 1",
    )
    self._method = method
    self._layer_membership = _validate_layer_membership(layer_membership, self._n)
Methods:
evaluate_actions
evaluate_actions(
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
    zeta: float,
    psi: float,
    actions: list[ControlAction]
    | tuple[ControlAction, ...],
) -> CounterfactualRollout

Compare no-action and intervened trajectories.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). omegas : FloatArray Natural frequencies in rad/s, shape (N,). knm : FloatArray Coupling matrix K_nm, shape (N, N). alpha : FloatArray Phase-lag matrix in radians, shape (N, N), or None for no lag. zeta : float External drive strength ζ. psi : float External drive reference phase Ψ in radians. actions : list[ControlAction] | tuple[ControlAction, ...] The control actions to apply or assess.

Returns

CounterfactualRollout The counterfactual rollout comparing no-action and intervened trajectories.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def evaluate_actions(
    self,
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
    zeta: float,
    psi: float,
    actions: list[ControlAction] | tuple[ControlAction, ...],
) -> CounterfactualRollout:
    """Compare no-action and intervened trajectories.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    omegas : FloatArray
        Natural frequencies in rad/s, shape ``(N,)``.
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    alpha : FloatArray
        Phase-lag matrix in radians, shape ``(N, N)``, or ``None`` for no lag.
    zeta : float
        External drive strength ``ζ``.
    psi : float
        External drive reference phase ``Ψ`` in radians.
    actions : list[ControlAction] | tuple[ControlAction, ...]
        The control actions to apply or assess.

    Returns
    -------
    CounterfactualRollout
        The counterfactual rollout comparing no-action and intervened trajectories.
    """
    phases, omegas, knm, alpha, zeta, psi = self._validate_inputs(
        phases,
        omegas,
        knm,
        alpha,
        zeta,
        psi,
    )
    action_tuple = tuple(actions)
    intervened = self.apply_actions(knm, alpha, zeta, psi, action_tuple)

    baseline_R, baseline_psi = self._rollout(phases, omegas, knm, alpha, zeta, psi)
    intervention_R, intervention_psi = self._rollout(
        phases,
        omegas,
        intervened.knm,
        intervened.alpha,
        intervened.zeta,
        intervened.psi,
    )

    baseline_arr = np.asarray(baseline_R, dtype=np.float64)
    intervention_arr = np.asarray(intervention_R, dtype=np.float64)
    return CounterfactualRollout(
        baseline_R=baseline_R,
        intervention_R=intervention_R,
        baseline_psi=baseline_psi,
        intervention_psi=intervention_psi,
        delta_R_final=float(intervention_arr[-1] - baseline_arr[-1]),
        delta_R_mean=float(np.mean(intervention_arr - baseline_arr)),
        delta_psi_final=_signed_phase_delta(
            intervention_psi[-1],
            baseline_psi[-1],
        ),
        actions=action_tuple,
    )
apply_actions
apply_actions(
    knm: FloatArray,
    alpha: FloatArray,
    zeta: float,
    psi: float,
    actions: tuple[ControlAction, ...],
) -> InterventionParameters

Apply supported supervisor actions to UPDE parameters.

Parameters

knm : FloatArray Coupling matrix K_nm, shape (N, N). alpha : FloatArray Phase-lag matrix in radians, shape (N, N), or None for no lag. zeta : float External drive strength ζ. psi : float External drive reference phase Ψ in radians. actions : tuple[ControlAction, ...] The control actions to apply or assess.

Returns

InterventionParameters The UPDE parameters after applying the actions.

Raises

ValueError If an action is unsupported.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def apply_actions(
    self,
    knm: FloatArray,
    alpha: FloatArray,
    zeta: float,
    psi: float,
    actions: tuple[ControlAction, ...],
) -> InterventionParameters:
    """Apply supported supervisor actions to UPDE parameters.

    Parameters
    ----------
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    alpha : FloatArray
        Phase-lag matrix in radians, shape ``(N, N)``, or ``None`` for no lag.
    zeta : float
        External drive strength ``ζ``.
    psi : float
        External drive reference phase ``Ψ`` in radians.
    actions : tuple[ControlAction, ...]
        The control actions to apply or assess.

    Returns
    -------
    InterventionParameters
        The UPDE parameters after applying the actions.

    Raises
    ------
    ValueError
        If an action is unsupported.
    """
    next_knm = np.array(_coerce_float_array("knm", knm), copy=True)
    next_alpha = np.array(_coerce_float_array("alpha", alpha), copy=True)
    if next_knm.shape != (self._n, self._n):
        raise ValueError(
            f"knm.shape={next_knm.shape}, expected {(self._n, self._n)}"
        )
    if next_alpha.shape != (self._n, self._n):
        raise ValueError(
            f"alpha.shape={next_alpha.shape}, expected {(self._n, self._n)}"
        )
    next_zeta = _require_finite_real(zeta, name="zeta")
    next_psi = _require_finite_real(psi, name="psi")

    for action in actions:
        action_value = _require_finite_real(action.value, name="action.value")
        if action.knob == "K":
            _apply_matrix_delta(
                next_knm, action.scope, action_value, self._layer_membership
            )
        elif action.knob == "alpha":
            _apply_matrix_delta(
                next_alpha, action.scope, action_value, self._layer_membership
            )
        elif action.knob == "zeta":
            next_zeta += action_value
        elif action.knob in {"Psi", "psi"}:
            next_psi = (next_psi + action_value) % TWO_PI
        else:
            msg = f"unsupported causal intervention knob {action.knob!r}"
            raise ValueError(msg)

    np.fill_diagonal(next_knm, 0.0)
    np.fill_diagonal(next_alpha, 0.0)
    return InterventionParameters(
        knm=next_knm,
        alpha=next_alpha,
        zeta=next_zeta,
        psi=next_psi,
    )

Functions:

learn_causal_graph

learn_causal_graph(
    trace: dict[str, list[float]],
    rollouts: list[CounterfactualRollout]
    | tuple[CounterfactualRollout, ...] = (),
    *,
    lag: int = 1,
    min_abs_weight: float = 1e-06,
) -> CausalGraphEstimate

Estimate a signed live causal graph from traces and interventions.

Trace edges use lagged linear influence from source[t] to target[t + lag] - target[t]. Intervention edges summarise paired counterfactual rollouts as explicit do(knob:scope) -> R effects. The estimate is intentionally lightweight and audit-first; it is not a formal do-calculus proof.

Parameters

trace : dict[str, list[float]] Signal trace keyed by variable name, each a sequence of floats. rollouts : list[CounterfactualRollout] | tuple[CounterfactualRollout, ...] Counterfactual rollouts used to estimate causal edges. lag : int Lag in samples for the causal estimate. min_abs_weight : float Minimum absolute edge weight retained in the graph.

Returns

CausalGraphEstimate The estimated signed causal graph.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def learn_causal_graph(
    trace: dict[str, list[float]],
    rollouts: list[CounterfactualRollout] | tuple[CounterfactualRollout, ...] = (),
    *,
    lag: int = 1,
    min_abs_weight: float = 1e-6,
) -> CausalGraphEstimate:
    """Estimate a signed live causal graph from traces and interventions.

    Trace edges use lagged linear influence from ``source[t]`` to
    ``target[t + lag] - target[t]``. Intervention edges summarise paired
    counterfactual rollouts as explicit ``do(knob:scope) -> R`` effects. The
    estimate is intentionally lightweight and audit-first; it is not a formal
    do-calculus proof.

    Parameters
    ----------
    trace : dict[str, list[float]]
        Signal trace keyed by variable name, each a sequence of floats.
    rollouts : list[CounterfactualRollout] | tuple[CounterfactualRollout, ...]
        Counterfactual rollouts used to estimate causal edges.
    lag : int
        Lag in samples for the causal estimate.
    min_abs_weight : float
        Minimum absolute edge weight retained in the graph.

    Returns
    -------
    CausalGraphEstimate
        The estimated signed causal graph.
    """
    trace_arrays = _validate_causal_trace(trace, lag, min_abs_weight)
    nodes = tuple(trace_arrays)
    edges: list[CausalInfluenceEdge] = []
    for source in nodes:
        source_values = trace_arrays[source]
        source_window = source_values[:-lag]
        for target in nodes:
            if source == target:
                continue
            target_values = trace_arrays[target]
            target_delta = target_values[lag:] - target_values[:-lag]
            weight, confidence = _lagged_linear_effect(source_window, target_delta)
            if abs(weight) >= min_abs_weight:
                edges.append(
                    CausalInfluenceEdge(
                        source=source,
                        target=target,
                        weight=weight,
                        confidence=confidence,
                        lag=lag,
                        evidence="lagged_trace",
                    )
                )

    intervention_nodes: list[str] = []
    for rollout in rollouts:
        for action in rollout.actions:
            source = f"do({action.knob}:{action.scope})"
            intervention_nodes.append(source)
            effect_scale = action.value if action.value != 0.0 else 1.0
            weight = float(rollout.delta_R_mean / effect_scale)
            if abs(weight) < min_abs_weight:
                continue
            magnitude = abs(rollout.delta_R_mean) + abs(rollout.delta_R_final)
            confidence = min(1.0, magnitude / max(min_abs_weight, 1e-12))
            edges.append(
                CausalInfluenceEdge(
                    source=source,
                    target="R",
                    weight=weight,
                    confidence=confidence,
                    lag=0,
                    evidence="counterfactual_rollout",
                )
            )

    all_nodes = tuple(dict.fromkeys((*nodes, *intervention_nodes, "R")))
    edges.sort(key=lambda edge: (edge.source, edge.target, edge.evidence))
    return CausalGraphEstimate(
        nodes=all_nodes,
        edges=tuple(edges),
        lag=lag,
        min_abs_weight=min_abs_weight,
    )

build_temporal_causal_hypergraph_experiment

build_temporal_causal_hypergraph_experiment(
    trace: dict[str, list[float]],
    candidate_hyperedges: list[dict[str, object]]
    | tuple[dict[str, object], ...],
    *,
    lag: int = 1,
    min_abs_weight: float = 1e-06,
    required_baseline_margin: float = 0.0,
) -> dict[str, object]

Build a research-only temporal-causal hypergraph experiment manifest.

The manifest compares candidate time-symmetric hyperedges against a deterministic family of conventional causal baselines: lagged-linear graph edges, lagged Pearson correlation, lagged-delta correlation, Granger-style residual improvement, and target persistence. It never permits production claims, hot-patching, or actuation; baseline failure keeps all candidates blocked as research evidence only.

Parameters

trace : dict[str, list[float]] Signal trace keyed by variable name, each a sequence of floats. candidate_hyperedges : list[dict[str, object]] | tuple[dict[str, object], ...] Candidate causal hyperedges to test. lag : int Lag in samples for the causal estimate. min_abs_weight : float Minimum absolute edge weight retained in the graph. required_baseline_margin : float Minimum baseline margin a hyperedge must beat.

Returns

dict[str, object] The temporal-causal hypergraph experiment manifest.

Raises

ValueError If the trace or candidate hyperedges are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/causal.py
def build_temporal_causal_hypergraph_experiment(
    trace: dict[str, list[float]],
    candidate_hyperedges: list[dict[str, object]] | tuple[dict[str, object], ...],
    *,
    lag: int = 1,
    min_abs_weight: float = 1e-6,
    required_baseline_margin: float = 0.0,
) -> dict[str, object]:
    """Build a research-only temporal-causal hypergraph experiment manifest.

    The manifest compares candidate time-symmetric hyperedges against a
    deterministic family of conventional causal baselines: lagged-linear graph
    edges, lagged Pearson correlation, lagged-delta correlation,
    Granger-style residual improvement, and target persistence. It never
    permits production claims, hot-patching, or actuation; baseline failure
    keeps all candidates blocked as research evidence only.

    Parameters
    ----------
    trace : dict[str, list[float]]
        Signal trace keyed by variable name, each a sequence of floats.
    candidate_hyperedges : list[dict[str, object]] | tuple[dict[str, object], ...]
        Candidate causal hyperedges to test.
    lag : int
        Lag in samples for the causal estimate.
    min_abs_weight : float
        Minimum absolute edge weight retained in the graph.
    required_baseline_margin : float
        Minimum baseline margin a hyperedge must beat.

    Returns
    -------
    dict[str, object]
        The temporal-causal hypergraph experiment manifest.

    Raises
    ------
    ValueError
        If the trace or candidate hyperedges are invalid.
    """
    if not np.isfinite(required_baseline_margin) or required_baseline_margin < 0.0:
        raise ValueError("required_baseline_margin must be finite and non-negative")
    baseline = learn_causal_graph(trace, lag=lag, min_abs_weight=min_abs_weight)
    candidates = _validated_temporal_hyperedges(candidate_hyperedges)
    baseline_family = _causal_baseline_family(
        trace,
        lag=lag,
        min_abs_weight=min_abs_weight,
        graph=baseline,
    )
    strongest_baseline = max(
        baseline_family,
        key=lambda record: (
            float(record["score"]),
            str(record["name"]),
        ),
    )
    baseline_score = float(strongest_baseline["score"])
    accepted: list[dict[str, object]] = []
    evaluated: list[dict[str, object]] = []
    for candidate in candidates:
        score = candidate["score"]
        if not isinstance(score, int | float) or isinstance(score, bool):
            raise ValueError("candidate score must be finite")
        advantage = float(score) - baseline_score
        record = {
            **candidate,
            "baseline_score": baseline_score,
            "baseline_margin": advantage,
            "accepted": advantage > required_baseline_margin,
        }
        evaluated.append(record)
        if record["accepted"]:
            accepted.append(record)

    blocked_reasons: list[str] = []
    baseline_beaten = bool(accepted)
    if not baseline_beaten:
        blocked_reasons.append("conventional_causal_baseline_not_beaten")
    manifest: dict[str, object] = {
        "schema": "scpn_temporal_causal_hypergraph_experiment_v1",
        "research_only": True,
        "production_claim_permitted": False,
        "hot_patch_permitted": False,
        "actuation_permitted": False,
        "baseline_beaten": baseline_beaten,
        "blocked_reasons": blocked_reasons,
        "required_baseline_margin": float(required_baseline_margin),
        "baseline": {
            "edge_count": len(baseline.edges),
            "node_count": len(baseline.nodes),
            "score": baseline_score,
            "strongest_baseline": strongest_baseline["name"],
            "baseline_family": baseline_family,
            "lag": baseline.lag,
            "min_abs_weight": baseline.min_abs_weight,
            "edges": [edge.to_audit_record() for edge in baseline.edges],
        },
        "candidate_hyperedge_count": len(evaluated),
        "accepted_hyperedge_count": len(accepted),
        "evaluated_hyperedges": evaluated,
        "accepted_hyperedges": accepted,
    }
    manifest["experiment_sha256"] = _stable_json_hash(manifest)
    return manifest

Policy Rules (Declarative)

Declarative rules loaded from YAML/JSON configuration.

Data model

PolicyCondition(metric: str, layer: int | None, op: str, threshold: float)
CompoundCondition(conditions: list[PolicyCondition], logic: str = "AND")
PolicyAction(knob: str, scope: str, value: float, ttl_s: float)
PolicyRule(
    name: str,
    regimes: list[str],       # active in these regimes
    condition: PolicyCondition | CompoundCondition,
    actions: list[PolicyAction],
    cooldown_s: float = 0.0,  # min seconds between firings
    max_fires: int = 0,       # 0 = unlimited
)

STL Monitors In Policy YAML

Policy files may also declare reviewable Signal Temporal Logic monitors under top-level stl_monitors. These monitors do not emit control actions directly; they evaluate scalar traces and return audit records that can be used by the runtime gate or safety review job.

rules: []
stl_monitors:
  - name: keep_sync
    spec: always (R >= 0.3)
    severity: hard
  - name: eventual_recovery
    spec: eventually (R >= 0.8)
from scpn_phase_orchestrator.supervisor.policy_rules import (
    evaluate_policy_stl_specs,
    load_policy_stl_specs,
)

specs = load_policy_stl_specs("policy.yaml")
results = evaluate_policy_stl_specs(specs, {"R": [0.2, 0.4, 0.9]})
audit_payloads = [result.to_audit_record() for result in results]

PolicyEngine

engine = PolicyEngine(rules)
engine.advance_clock(dt)
actions = engine.evaluate(regime, upde_state, good_layers, bad_layers)

Rules are evaluated in list order. Each rule fires if: 1. Current regime is in rule.regimes 2. Condition evaluates True against UPDEState metrics 3. Cooldown has expired since last firing 4. max_fires not exceeded

load_policy_rules(path) loads rules from YAML/JSON file.

policy_rules

Policy DSL records, loaders, STL monitors, and bounded rule evaluation.

This module validates policy conditions, compound logic, action declarations, cooldowns, max-fire limits, and STL monitor specifications before evaluation. PolicyEngine returns ControlAction proposals when regime and metric conditions match, while loaders cap rule, condition, and action counts. Policy evaluation is local and does not apply actuation.

Classes

PolicyCondition dataclass

PolicyCondition(
    metric: str,
    layer: int | None,
    op: str,
    threshold: float,
)

List the metric names known to the policy DSL.

Known metrics: R, R_good, R_bad, stability_proxy, pac_max, mean_amplitude, subcritical_fraction, amplitude_spread (per-layer), mean_amplitude_layer (per-layer).

CompoundCondition dataclass

CompoundCondition(
    conditions: list[PolicyCondition], logic: str = "AND"
)

AND/OR combinator over multiple PolicyConditions.

PolicyAction dataclass

PolicyAction(
    knob: str, scope: str, value: float, ttl_s: float
)

Action emitted by a policy rule: knob, scope, target value, and TTL.

PolicyRule dataclass

PolicyRule(
    name: str,
    regimes: list[str],
    condition: PolicyCondition | CompoundCondition,
    actions: list[PolicyAction],
    cooldown_s: float = 0.0,
    max_fires: int = 0,
)

Named rule: fires actions when regime and condition match.

PolicySTLSpec dataclass

PolicySTLSpec(name: str, spec: str, severity: str = 'soft')

Named STL monitor declared by the policy DSL.

PolicySTLResult dataclass

PolicySTLResult(
    name: str, severity: str, result: STLTraceResult
)

Policy-level STL result with monitor name and severity.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe policy STL audit record.

Returns

dict[str, object] Return a JSON-safe policy STL audit record.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe policy STL audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe policy STL audit record.
    """
    payload = self.result.to_audit_record()
    payload["name"] = self.name
    payload["severity"] = self.severity
    return payload

PolicySTLAutomaton dataclass

PolicySTLAutomaton(
    name: str,
    severity: str,
    automaton: STLMonitoringAutomaton,
)

Policy-level synthesized STL automaton with monitor name and severity.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe policy STL automaton audit record.

Returns

dict[str, object] Return a JSON-safe policy STL automaton audit record.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe policy STL automaton audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe policy STL automaton audit record.
    """
    payload = self.automaton.to_audit_record()
    payload["name"] = self.name
    payload["severity"] = self.severity
    return payload

PolicyEngine

PolicyEngine(rules: list[PolicyRule])

Evaluate domainpack policy rules against current state.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def __init__(self, rules: list[PolicyRule]) -> None:
    if not isinstance(rules, list) or not rules:
        raise ValueError("rules must be a non-empty list of PolicyRule objects")
    if not all(isinstance(rule, PolicyRule) for rule in rules):
        raise ValueError("rules must contain only PolicyRule objects")
    self._rules = list(rules)
    self._fire_counts: dict[str, int] = {}
    self._last_fire_t: dict[str, float] = {}
    self._clock: float = 0.0
Methods:
advance_clock
advance_clock(dt: float) -> None

Advance the internal clock used for cooldown tracking.

Parameters

dt : float Integration step size.

Raises

ValueError If dt is not positive.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def advance_clock(self, dt: float) -> None:
    """Advance the internal clock used for cooldown tracking.

    Parameters
    ----------
    dt : float
        Integration step size.

    Raises
    ------
    ValueError
        If ``dt`` is not positive.
    """
    if isinstance(dt, bool) or not isinstance(dt, Real):
        raise ValueError(f"dt must be a finite non-negative real, got {dt!r}")
    dt = float(dt)
    if not isfinite(dt) or dt < 0.0:
        raise ValueError(f"dt must be a finite non-negative real, got {dt!r}")
    self._clock += dt
evaluate
evaluate(
    regime: Regime,
    upde_state: UPDEState,
    good_layers: list[int],
    bad_layers: list[int],
) -> list[ControlAction]

Evaluate all rules against current state and return triggered actions.

Parameters

regime : Regime The current control regime. upde_state : UPDEState The current UPDE state. good_layers : list[int] Indices of the maintain (good) layers. bad_layers : list[int] Indices of the suppress (bad) layers.

Returns

list[ControlAction] The control actions triggered by the matching rules.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def evaluate(
    self,
    regime: Regime,
    upde_state: UPDEState,
    good_layers: list[int],
    bad_layers: list[int],
) -> list[ControlAction]:
    """Evaluate all rules against current state and return triggered actions.

    Parameters
    ----------
    regime : Regime
        The current control regime.
    upde_state : UPDEState
        The current UPDE state.
    good_layers : list[int]
        Indices of the maintain (good) layers.
    bad_layers : list[int]
        Indices of the suppress (bad) layers.

    Returns
    -------
    list[ControlAction]
        The control actions triggered by the matching rules.
    """
    actions: list[ControlAction] = []
    for rule in self._rules:
        if regime.value.upper() not in rule.regimes:
            continue
        if not self._check_condition(
            rule.condition, upde_state, good_layers, bad_layers
        ):
            continue
        if rule.cooldown_s > 0:
            last = self._last_fire_t.get(rule.name, -rule.cooldown_s - 1)
            if self._clock - last < rule.cooldown_s:
                continue
        fires = self._fire_counts.get(rule.name, 0)
        if rule.max_fires > 0 and fires >= rule.max_fires:
            continue
        self._fire_counts[rule.name] = self._fire_counts.get(rule.name, 0) + 1
        self._last_fire_t[rule.name] = self._clock
        for pa in rule.actions:
            actions.append(
                ControlAction(
                    knob=pa.knob,
                    scope=pa.scope,
                    value=pa.value,
                    ttl_s=pa.ttl_s,
                    justification=f"policy rule: {rule.name}",
                )
            )
    return actions

Functions:

load_policy_stl_specs

load_policy_stl_specs(
    path: str | Path,
) -> list[PolicySTLSpec]

Load top-level stl_monitors declarations from a policy YAML file.

Parameters

path : str | Path Filesystem path to the policy YAML file.

Returns

list[PolicySTLSpec] The STL monitor declarations from the policy YAML.

Raises

ValueError If the policy YAML is malformed.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def load_policy_stl_specs(path: str | Path) -> list[PolicySTLSpec]:
    """Load top-level ``stl_monitors`` declarations from a policy YAML file.

    Parameters
    ----------
    path : str | Path
        Filesystem path to the policy YAML file.

    Returns
    -------
    list[PolicySTLSpec]
        The STL monitor declarations from the policy YAML.

    Raises
    ------
    ValueError
        If the policy YAML is malformed.
    """
    import yaml

    path = Path(path)
    try:
        raw = path.read_text(encoding="utf-8")
    except OSError as exc:
        reason = exc.strerror or type(exc).__name__
        raise ValueError(f"cannot read policy rules: {reason}") from None
    try:
        data = yaml.safe_load(raw)
    except (RecursionError, OverflowError, UnicodeError, ValueError, yaml.YAMLError):
        raise ValueError("policy rules YAML parse error") from None
    if not isinstance(data, dict) or "stl_monitors" not in data:
        return []
    monitors = _require_sequence(data["stl_monitors"], "stl_monitors")
    if len(monitors) > _MAX_POLICY_RULES:
        _policy_error("too many stl monitors")
    return [_parse_stl_spec(item) for item in monitors]

evaluate_policy_stl_specs

evaluate_policy_stl_specs(
    specs: list[PolicySTLSpec] | tuple[PolicySTLSpec, ...],
    trace: dict[str, list[float]],
) -> list[PolicySTLResult]

Evaluate policy-declared STL monitors over a scalar trace.

Parameters

specs : list[PolicySTLSpec] | tuple[PolicySTLSpec, ...] The STL monitor specifications. trace : dict[str, list[float]] Signal trace keyed by variable name, each a list of floats.

Returns

list[PolicySTLResult] The per-monitor STL evaluation results.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def evaluate_policy_stl_specs(
    specs: list[PolicySTLSpec] | tuple[PolicySTLSpec, ...],
    trace: dict[str, list[float]],
) -> list[PolicySTLResult]:
    """Evaluate policy-declared STL monitors over a scalar trace.

    Parameters
    ----------
    specs : list[PolicySTLSpec] | tuple[PolicySTLSpec, ...]
        The STL monitor specifications.
    trace : dict[str, list[float]]
        Signal trace keyed by variable name, each a list of floats.

    Returns
    -------
    list[PolicySTLResult]
        The per-monitor STL evaluation results.
    """
    return [
        PolicySTLResult(
            name=spec.name,
            severity=spec.severity,
            result=STLMonitor(spec.spec).evaluate_result(trace),
        )
        for spec in specs
    ]

synthesise_policy_stl_automata

synthesise_policy_stl_automata(
    specs: list[PolicySTLSpec] | tuple[PolicySTLSpec, ...],
    trace: dict[str, list[float]],
) -> list[PolicySTLAutomaton]

Synthesise audit automata for policy-declared builtin STL monitors.

Parameters

specs : list[PolicySTLSpec] | tuple[PolicySTLSpec, ...] The STL monitor specifications. trace : dict[str, list[float]] Signal trace keyed by variable name, each a list of floats.

Returns

list[PolicySTLAutomaton] The audit automata for the policy STL monitors.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def synthesise_policy_stl_automata(
    specs: list[PolicySTLSpec] | tuple[PolicySTLSpec, ...],
    trace: dict[str, list[float]],
) -> list[PolicySTLAutomaton]:
    """Synthesise audit automata for policy-declared builtin STL monitors.

    Parameters
    ----------
    specs : list[PolicySTLSpec] | tuple[PolicySTLSpec, ...]
        The STL monitor specifications.
    trace : dict[str, list[float]]
        Signal trace keyed by variable name, each a list of floats.

    Returns
    -------
    list[PolicySTLAutomaton]
        The audit automata for the policy STL monitors.
    """
    return [
        PolicySTLAutomaton(
            name=spec.name,
            severity=spec.severity,
            automaton=synthesise_stl_monitoring_automaton(spec.spec, trace),
        )
        for spec in specs
    ]

load_policy_rules

load_policy_rules(path: str | Path) -> list[PolicyRule]

Load policy rules from a YAML file.

Supports both v0.1 (single condition/action) and v0.2 (compound conditions with logic, action chains) formats.

Parameters

path : str | Path Filesystem path to the policy YAML file.

Returns

list[PolicyRule] The policy rules loaded from the YAML file.

Raises

ValueError If the policy YAML is malformed.

Source code in src/scpn_phase_orchestrator/supervisor/policy_rules.py
def load_policy_rules(path: str | Path) -> list[PolicyRule]:
    """Load policy rules from a YAML file.

    Supports both v0.1 (single condition/action) and v0.2 (compound
    conditions with logic, action chains) formats.

    Parameters
    ----------
    path : str | Path
        Filesystem path to the policy YAML file.

    Returns
    -------
    list[PolicyRule]
        The policy rules loaded from the YAML file.

    Raises
    ------
    ValueError
        If the policy YAML is malformed.
    """
    import yaml

    path = Path(path)
    try:
        raw = path.read_text(encoding="utf-8")
    except OSError as exc:
        reason = exc.strerror or type(exc).__name__
        raise ValueError(f"cannot read policy rules: {reason}") from None
    try:
        data = yaml.safe_load(raw)
    except (RecursionError, OverflowError, UnicodeError, ValueError, yaml.YAMLError):
        raise ValueError("policy rules YAML parse error") from None
    if not isinstance(data, dict) or "rules" not in data:
        return []
    rule_data = _require_sequence(data["rules"], "rules")
    if len(rule_data) > _MAX_POLICY_RULES:
        _policy_error("too many rules")
    rules: list[PolicyRule] = []
    for raw_rule in rule_data:
        r = _require_mapping(raw_rule, "rule")
        # --- condition(s) ---
        if "conditions" in r:
            conditions = _require_sequence(r["conditions"], "rule.conditions")
            if not conditions:
                _policy_error("rule.conditions must not be empty")
            if len(conditions) > _MAX_CONDITIONS_PER_RULE:
                _policy_error("too many rule conditions")
            logic = _compound_logic(r.get("logic", "AND"))
            cond: PolicyCondition | CompoundCondition = CompoundCondition(
                conditions=[_parse_condition(c) for c in conditions],
                logic=logic,
            )
        else:
            cond = _parse_condition(_require_field(r, "condition", "rule"))

        # --- action(s) ---
        if "actions" in r:
            actions = _require_sequence(r["actions"], "rule.actions")
            if not actions:
                _policy_error("rule.actions must not be empty")
            if len(actions) > _MAX_ACTIONS_PER_RULE:
                _policy_error("too many rule actions")
            action_list = [_parse_action(a) for a in actions]
        else:
            action_list = [_parse_action(_require_field(r, "action", "rule"))]

        regimes = r.get("regime", [])
        if not isinstance(regimes, list) or not all(
            isinstance(item, str) and item for item in regimes
        ):
            _policy_error("rule.regime must be a list of non-empty strings")

        rules.append(
            PolicyRule(
                name=_require_text(r, "name", "rule"),
                regimes=[s.upper() for s in regimes],
                condition=cond,
                actions=action_list,
                cooldown_s=_non_negative_float(
                    r.get("cooldown_s", 0.0), "rule.cooldown_s"
                ),
                max_fires=_non_negative_int(r.get("max_fires", 0), "rule.max_fires"),
            )
        )
    return rules

Policy Diagnostics

Dry-run helpers for validating policy reachability, overlap, cooldown, and action output before a rule set is allowed into a live supervisor path.

policy_diagnostics

Offline policy-rule dry-run diagnostics over audit-log style entries.

The dry-run path reconstructs reduced UPDEState snapshots, evaluates policy rules without applying actions, tracks rule/action fire counts, and reports unreachable rules, overlapping rule firings, and action collisions. It mutates only the local PolicyEngine clock used for cooldown simulation and never touches runtime supervisor or actuation state.

Classes

PolicyDryRunStep dataclass

PolicyDryRunStep(
    step: int,
    regime: str,
    fired_rules: tuple[str, ...],
    actions: tuple[str, ...],
)

One audit step and the policy rules that fired on it.

PolicyDryRunReport dataclass

PolicyDryRunReport(
    steps: int,
    rules: tuple[str, ...],
    fire_counts: dict[str, int],
    action_counts: dict[str, int],
    unreachable_rules: tuple[str, ...],
    overlapping_steps: tuple[int, ...],
    action_collision_steps: tuple[int, ...],
    step_reports: tuple[PolicyDryRunStep, ...],
)

Summary of replayed policy behaviour over an audit log.

Functions:

dry_run_policy_rules

dry_run_policy_rules(
    rules: list[PolicyRule],
    entries: list[dict[str, Any]],
    *,
    good_layers: list[int],
    bad_layers: list[int],
) -> PolicyDryRunReport

Replay policy rules over audit steps without applying actuation.

Parameters

rules : list[PolicyRule] The policy rules to evaluate. entries : list[dict[str, Any]] Audit-log step entries to replay. good_layers : list[int] Indices of the maintain (good) layers. bad_layers : list[int] Indices of the suppress (bad) layers.

Returns

PolicyDryRunReport The policy dry-run report.

Source code in src/scpn_phase_orchestrator/supervisor/policy_diagnostics.py
def dry_run_policy_rules(
    rules: list[PolicyRule],
    entries: list[dict[str, Any]],
    *,
    good_layers: list[int],
    bad_layers: list[int],
) -> PolicyDryRunReport:
    """Replay policy rules over audit steps without applying actuation.

    Parameters
    ----------
    rules : list[PolicyRule]
        The policy rules to evaluate.
    entries : list[dict[str, Any]]
        Audit-log step entries to replay.
    good_layers : list[int]
        Indices of the maintain (good) layers.
    bad_layers : list[int]
        Indices of the suppress (bad) layers.

    Returns
    -------
    PolicyDryRunReport
        The policy dry-run report.
    """
    engine = PolicyEngine(rules)
    rule_names = tuple(rule.name for rule in rules)
    fire_counts = dict.fromkeys(rule_names, 0)
    action_counts: dict[str, int] = {}
    step_reports: list[PolicyDryRunStep] = []
    overlapping_steps: list[int] = []
    action_collision_steps: list[int] = []

    for entry in _step_entries(entries):
        step_no = int(entry.get("step", 0))
        regime = _regime_from_entry(entry)
        state = _state_from_entry(entry)
        actions = engine.evaluate(regime, state, good_layers, bad_layers)
        fired_rules = tuple(
            _rule_name_from_justification(action.justification) for action in actions
        )
        distinct_rules = tuple(dict.fromkeys(fired_rules))
        if len(distinct_rules) > 1:
            overlapping_steps.append(step_no)

        action_keys = tuple(f"{action.knob}:{action.scope}" for action in actions)
        if len(set(action_keys)) < len(action_keys):
            action_collision_steps.append(step_no)
        for rule_name in fired_rules:
            fire_counts[rule_name] = fire_counts.get(rule_name, 0) + 1
        for action_key in action_keys:
            action_counts[action_key] = action_counts.get(action_key, 0) + 1

        step_reports.append(
            PolicyDryRunStep(
                step=step_no,
                regime=regime.value,
                fired_rules=distinct_rules,
                actions=action_keys,
            )
        )
        engine.advance_clock(1.0)

    unreachable = tuple(name for name in rule_names if fire_counts.get(name, 0) == 0)
    return PolicyDryRunReport(
        steps=len(step_reports),
        rules=rule_names,
        fire_counts=fire_counts,
        action_counts=action_counts,
        unreachable_rules=unreachable,
        overlapping_steps=tuple(overlapping_steps),
        action_collision_steps=tuple(action_collision_steps),
        step_reports=tuple(step_reports),
    )

Formal Export

Export helpers translate Petri-net, policy-rule, and policy-declared STL surfaces into model-checker artefacts for independent safety analysis. PRISM exports remain the default; TLA+ modules are available for protocol and policy transition-system checks.

The CLI supports:

spo formal-export domainpacks/my_domain/binding_spec.yaml --export protocol
spo formal-export domainpacks/my_domain/binding_spec.yaml --export policy
spo formal-export domainpacks/my_domain/binding_spec.yaml --export stl
spo formal-export domainpacks/my_domain/binding_spec.yaml --export protocol-tla
spo formal-export domainpacks/my_domain/binding_spec.yaml --export policy-tla
spo formal-export domainpacks/my_domain/binding_spec.yaml --export policy-smt
spo formal-export domainpacks/my_domain/binding_spec.yaml --export package

--export stl reads stl_monitors from the sibling policy.yaml by default and emits signal constants plus satisfied/violated labels for the builtin STL subset. This is a model-checker linkage surface; full temporal automata synthesis remains future work. --export protocol-tla emits a bounded TLA+ module with Petri places as variables, transition guards as constants, Init, Next, Spec, and Safety == TypeOK. --export policy-tla emits bounded rule-fire counters plus reachability predicates for fired rules and emitted actions. --export policy-smt emits an SMT-LIB v2 feasibility model for Z3: the model declares the active regime, metric inputs, bounded rule-fire counters, rule firing predicates, emitted-action predicates, and a final check-sat envelope asking whether at least one rule can fire under the declared guards. --export package emits a JSON formal verification package manifest that binds protocol PRISM/TLA, policy PRISM, and generated policy SMT-LIB artefact hashes to named safety properties and external PRISM/TLC/Z3 command records. The package API also accepts reviewed Promela and SMT-LIB text artefacts through FormalTextArtifact, linking them to non-executing SPIN and Z3 command/readiness manifests under the same hash and disabled-execution contract. The package does not run model checkers; all command records keep execution_permitted=false. Add --include-checker-readiness to append non-executing checker availability records to that JSON; --checker-path executable=/path can make CI readiness evidence deterministic, and --checker-path executable= forces a missing checker record without invoking anything. build_runtime_control_certificate() turns a package, checker readiness records, externally reviewed checker result records, and finite runtime bounds into a deterministic FormalRuntimeCertificate. The certificate is the runtime handoff contract for verifiable control: every required property must have a matching available checker and a passed result bound to the exact package hash. Missing, failed, stale, or unavailable evidence produces status="blocked". Even status="verified_non_actuating" keeps actuation_permitted=false; it is an auditable precondition for operator review or a separate runtime monitor, not permission to execute hardware controls. Remote CI owns the first external execution lane through formal-model-checkers.yml, which installs SPIN and Z3, materialises reviewed Promela/SMT-LIB smoke artefacts, validates disabled package/readiness metadata, and runs those external checkers only under the CI-only execution guard. The same lane now also materialises safety-domain packages for cardiac_rhythm, chemical_reactor, power_grid, pll_clock, autonomous_vehicles, satellite_constellation, power_safety_nchannel, traffic_flow, swarm_robotics, manufacturing_spc, and robotic_cpg. Each domain package binds a SPIN operator-approval gate and a Z3 hard-bound feasibility artefact derived from the domainpack safety boundaries, preserving the disabled runtime-execution contract while allowing remote CI to execute the external checker commands in an isolated environment.

For builtin STL automata, synthesise_stl_controller_candidates() provides a non-actuating controller-synthesis bridge. It proposes signal-level candidate actions from the weakest violated predicate and records actuating=False; the proposal is an audit artefact, not a live controller or bypass around policy and actuation safety gates. project_stl_controller_candidates() can then map those candidates through explicit policy-approved projection templates and the standard ActionProjector, yielding bounded ControlAction proposals while still recording actuating=False. synthesise_stl_closed_loop_plan() combines those two stages into an offline closed-loop review artefact: it records the feedback signals, trace length, future review horizon, projected actions, and fail-closed blockers without mutating runtime state or enabling actuation.

formal_export

Formal-model exporters for Petri nets, policy rules, and STL monitors.

The exporter functions convert already-validated supervisor structures into PRISM or TLA+ text plus identifier maps, sanitising names and preserving metric, transition, rule, action, and STL mappings for auditability, split into responsibility modules (shared identifiers, verification package, runtime certificate, and per-formalism exporters) behind a stable re-export surface. Export routines are pure text generation; they do not invoke model checkers, write files, or change the source policy/Petri structures. shutil is re-exported so checker-availability tests resolve it on this package namespace.

Classes

PrismExport dataclass

PrismExport(
    model: str,
    place_names: dict[str, str],
    metric_names: dict[str, str],
    transition_names: dict[str, str],
    rule_names: dict[str, str] = dict(),
    action_names: dict[str, str] = dict(),
    stl_names: dict[str, str] = dict(),
)

PRISM model text plus the identifier mapping used during export.

TLAExport dataclass

TLAExport(
    module: str,
    place_names: dict[str, str],
    metric_names: dict[str, str],
    transition_names: dict[str, str],
    rule_names: dict[str, str] = dict(),
    action_names: dict[str, str] = dict(),
)

TLA+ module text plus the identifier mapping used during export.

FormalCheckerAvailability dataclass

FormalCheckerAvailability(
    property_name: str,
    checker: str,
    artifact_name: str,
    executable: str,
    command: tuple[str, ...],
    available: bool,
    resolved_path: str | None = None,
    status: str = "missing_executable",
    execution_permitted: bool = False,
)

Non-executing readiness record for one external checker command.

Methods:
__post_init__
__post_init__() -> None

Validate the non-executing checker availability contract.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def __post_init__(self) -> None:
    """Validate the non-executing checker availability contract."""
    _require_package_identifier(self.property_name, "availability property_name")
    if self.checker not in _SUPPORTED_CHECKERS:
        raise PolicyError(
            "availability checker must be 'prism', 'tlc', 'spin', or 'smt'"
        )
    _require_package_identifier(self.artifact_name, "availability artifact_name")
    if not isinstance(self.executable, str) or not self.executable.strip():
        raise PolicyError("availability executable must be a non-empty string")
    if any(ord(char) < 32 for char in self.executable):
        raise PolicyError(
            "availability executable must not contain control characters"
        )
    if self.resolved_path is not None and (
        not isinstance(self.resolved_path, str)
        or not self.resolved_path.strip()
        or any(ord(char) < 32 for char in self.resolved_path)
    ):
        raise PolicyError(
            "availability resolved_path must be None or a non-empty safe string"
        )
    if self.status not in {"ready_not_executed", "missing_executable"}:
        raise PolicyError("availability status is unsupported")
    if self.available != (self.status == "ready_not_executed"):
        raise PolicyError("availability status must match available flag")
    if not self.command:
        raise PolicyError("availability command must not be empty")
    for part in self.command:
        if not isinstance(part, str) or not part.strip():
            raise PolicyError(
                "availability command parts must be non-empty strings"
            )
        if any(ord(char) < 32 for char in part):
            raise PolicyError(
                "availability command parts must not contain control characters"
            )
    if self.execution_permitted:
        raise PolicyError("formal checker availability must not permit execution")
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe non-executing checker readiness record.

Returns

dict[str, object] Return a JSON-safe non-executing checker readiness record.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe non-executing checker readiness record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe non-executing checker readiness record.
    """
    return {
        "property_name": self.property_name,
        "checker": self.checker,
        "artifact_name": self.artifact_name,
        "executable": self.executable,
        "command": list(self.command),
        "available": self.available,
        "resolved_path": self.resolved_path,
        "status": self.status,
        "execution_permitted": self.execution_permitted,
    }

FormalCheckerResult dataclass

FormalCheckerResult(
    property_name: str,
    checker: str,
    artifact_name: str,
    package_hash: str,
    result_hash: str,
    status: str,
    passed: bool,
    detail: str = "",
    execution_permitted: bool = False,
)

Reviewed external checker result bound to one package hash.

Results are audit records supplied by CI or a human-reviewed verification workflow after materialising the package outside this library. The constructor validates identity, checker kind, package hash, and result hash; it never executes checkers and never grants actuation.

Methods:
__post_init__
__post_init__() -> None

Validate reviewed checker result identity and fail-closed status.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def __post_init__(self) -> None:
    """Validate reviewed checker result identity and fail-closed status."""
    _require_package_identifier(self.property_name, "checker result property_name")
    if self.checker not in _SUPPORTED_CHECKERS:
        raise PolicyError(
            "checker result checker must be 'prism', 'tlc', 'spin', or 'smt'"
        )
    _require_package_identifier(self.artifact_name, "checker result artifact_name")
    _require_sha256(self.package_hash, "checker result package_hash")
    _require_sha256(self.result_hash, "checker result_hash")
    if self.status not in {"passed", "failed", "not_run"}:
        raise PolicyError("checker result status is unsupported")
    if not isinstance(self.passed, bool):
        raise PolicyError("checker result passed flag must be a boolean")
    if self.passed != (self.status == "passed"):
        raise PolicyError("checker result status must match passed flag")
    if any(ord(char) < 32 for char in self.detail):
        raise PolicyError(
            "checker result detail must not contain control characters"
        )
    if self.execution_permitted:
        raise PolicyError("checker result execution must stay disabled")
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe external checker result record.

Returns

dict[str, object] Return a JSON-safe external checker result record.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe external checker result record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe external checker result record.
    """
    return {
        "property_name": self.property_name,
        "checker": self.checker,
        "artifact_name": self.artifact_name,
        "package_hash": self.package_hash,
        "result_hash": self.result_hash,
        "status": self.status,
        "passed": self.passed,
        "detail": self.detail,
        "execution_permitted": self.execution_permitted,
    }

FormalRuntimeCertificate dataclass

FormalRuntimeCertificate(
    certificate_name: str,
    package_name: str,
    package_hash: str,
    runtime_bounds: dict[str, float],
    checker_availability: tuple[
        FormalCheckerAvailability, ...
    ],
    checker_results: tuple[FormalCheckerResult, ...],
    required_property_count: int,
    passed_required_count: int,
    missing_required_properties: tuple[str, ...],
    failed_required_properties: tuple[str, ...],
    unavailable_checker_properties: tuple[str, ...],
    status: str,
    certificate_hash: str,
    actuation_permitted: bool = False,
)

Fail-closed runtime certificate for formal supervisor evidence.

A certificate binds a formal verification package, finite runtime bounds, checker readiness, and externally supplied checker results into one deterministic hash. A verified certificate is still non-actuating; it is an auditable precondition for operator review or a separate runtime monitor.

Methods:
__post_init__
__post_init__() -> None

Validate certificate integrity and non-actuating runtime status.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def __post_init__(self) -> None:
    """Validate certificate integrity and non-actuating runtime status."""
    _require_package_identifier(self.certificate_name, "certificate_name")
    _require_package_identifier(self.package_name, "certificate package_name")
    _require_sha256(self.package_hash, "certificate package_hash")
    _validate_runtime_bounds(self.runtime_bounds)
    for availability in self.checker_availability:
        if not isinstance(availability, FormalCheckerAvailability):
            raise PolicyError(
                "certificate checker_availability must contain availability records"
            )
    for result in self.checker_results:
        if not isinstance(result, FormalCheckerResult):
            raise PolicyError(
                "certificate checker_results must contain result records"
            )
    for field_name, value in (
        ("required_property_count", self.required_property_count),
        ("passed_required_count", self.passed_required_count),
    ):
        if isinstance(value, bool) or not isinstance(value, int) or value < 0:
            raise PolicyError(f"certificate {field_name} must be non-negative")
    if self.passed_required_count > self.required_property_count:
        raise PolicyError(
            "certificate passed_required_count must not exceed required count"
        )
    _validate_identifier_tuple(
        self.missing_required_properties,
        "certificate missing_required_properties",
    )
    _validate_identifier_tuple(
        self.failed_required_properties,
        "certificate failed_required_properties",
    )
    _validate_identifier_tuple(
        self.unavailable_checker_properties,
        "certificate unavailable_checker_properties",
    )
    if self.status not in {"verified_non_actuating", "blocked"}:
        raise PolicyError("certificate status is unsupported")
    if self.status == "verified_non_actuating" and (
        self.missing_required_properties
        or self.failed_required_properties
        or self.unavailable_checker_properties
        or self.passed_required_count != self.required_property_count
    ):
        raise PolicyError("verified certificate must have complete passed evidence")
    _require_sha256(self.certificate_hash, "certificate_hash")
    if self.actuation_permitted:
        raise PolicyError("formal runtime certificate must remain non-actuating")
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe runtime certificate.

Returns

dict[str, object] Return a deterministic JSON-safe runtime certificate.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe runtime certificate.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe runtime certificate.
    """
    return {
        "certificate_name": self.certificate_name,
        "package_name": self.package_name,
        "package_hash": self.package_hash,
        "runtime_bounds": dict(sorted(self.runtime_bounds.items())),
        "checker_availability": [
            item.to_audit_record() for item in self.checker_availability
        ],
        "checker_results": [
            item.to_audit_record() for item in self.checker_results
        ],
        "required_property_count": self.required_property_count,
        "passed_required_count": self.passed_required_count,
        "missing_required_properties": list(self.missing_required_properties),
        "failed_required_properties": list(self.failed_required_properties),
        "unavailable_checker_properties": list(self.unavailable_checker_properties),
        "status": self.status,
        "certificate_hash": self.certificate_hash,
        "actuation_permitted": self.actuation_permitted,
    }

FormalCheckerCommand dataclass

FormalCheckerCommand(
    property_name: str,
    checker: str,
    artifact_name: str,
    command: tuple[str, ...],
    execution_permitted: bool = False,
)

External model-checker command manifest for one property.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe external checker command record.

Returns

dict[str, object] Return a JSON-safe external checker command record.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/verification_package.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe external checker command record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe external checker command record.
    """
    return {
        "property_name": self.property_name,
        "checker": self.checker,
        "artifact_name": self.artifact_name,
        "command": list(self.command),
        "execution_permitted": self.execution_permitted,
    }

FormalSafetyProperty dataclass

FormalSafetyProperty(
    name: str,
    artifact_name: str,
    checker: str,
    expression: str,
    description: str = "",
    required: bool = True,
)

Named model-checking property bound to one exported artefact.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe formal property record.

Returns

dict[str, object] Return a JSON-safe formal property record.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/verification_package.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe formal property record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe formal property record.
    """
    return {
        "name": self.name,
        "artifact_name": self.artifact_name,
        "checker": self.checker,
        "expression": self.expression,
        "description": self.description,
        "required": self.required,
    }

FormalTextArtifact dataclass

FormalTextArtifact(artifact_type: str, text: str)

Reviewed external proof artefact text for package manifests.

This object lets operators add already-reviewed Promela or SMT-LIB artefacts to the same deterministic package contract as generated PRISM/TLA exports. It records text only; it does not generate, write, or execute external checker inputs.

FormalVerificationPackage dataclass

FormalVerificationPackage(
    package_name: str,
    artifact_hashes: dict[str, str],
    artifact_types: dict[str, str],
    properties: tuple[FormalSafetyProperty, ...],
    checker_commands: tuple[FormalCheckerCommand, ...],
    package_hash: str,
)

Deterministic bundle for external formal-verification workflows.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe package manifest.

Returns

dict[str, object] Return a JSON-safe package manifest.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/verification_package.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe package manifest.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe package manifest.
    """
    return {
        "package_name": self.package_name,
        "artifact_hashes": dict(sorted(self.artifact_hashes.items())),
        "artifact_types": dict(sorted(self.artifact_types.items())),
        "properties": [item.to_audit_record() for item in self.properties],
        "checker_commands": [
            command.to_audit_record() for command in self.checker_commands
        ],
        "package_hash": self.package_hash,
    }

Functions:

export_petri_net_prism

export_petri_net_prism(
    net: PetriNet,
    initial_marking: Marking,
    *,
    module_name: str = "spo_petri",
    max_tokens: int | None = None,
) -> PrismExport

Serialise a Petri net into a bounded PRISM MDP model.

Guard metrics become PRISM constants, so safety properties can be checked over scenario-specific metric assignments without changing the net model.

Parameters

net : PetriNet The Petri net to export. initial_marking : Marking The initial Petri net marking. module_name : str Name of the emitted model-checker module. max_tokens : int | None Maximum token bound per place, or None.

Returns

PrismExport The bounded PRISM MDP export of the Petri net.

Raises

PolicyError If the net or bounds violate the export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/petri_export.py
def export_petri_net_prism(
    net: PetriNet,
    initial_marking: Marking,
    *,
    module_name: str = "spo_petri",
    max_tokens: int | None = None,
) -> PrismExport:
    """Serialise a Petri net into a bounded PRISM MDP model.

    Guard metrics become PRISM constants, so safety properties can be checked
    over scenario-specific metric assignments without changing the net model.

    Parameters
    ----------
    net : PetriNet
        The Petri net to export.
    initial_marking : Marking
        The initial Petri net marking.
    module_name : str
        Name of the emitted model-checker module.
    max_tokens : int | None
        Maximum token bound per place, or ``None``.

    Returns
    -------
    PrismExport
        The bounded PRISM MDP export of the Petri net.

    Raises
    ------
    PolicyError
        If the net or bounds violate the export policy.
    """
    if not net.place_names:
        raise PolicyError("cannot export Petri net without places")
    if max_tokens is not None and max_tokens < 1:
        raise PolicyError("max_tokens must be >= 1")

    place_names = _place_mapping(net)
    transition_names = _transition_mapping(net)
    metric_names = _metric_mapping(net)
    token_bound = max_tokens or _token_upper_bound(net, initial_marking, minimum=1)
    module_identifier = _identifier(module_name, prefix="module")

    lines = [
        "mdp",
        "",
        "// Generated from SCPN PetriNet for PRISM model checking.",
    ]
    if any(raw != mapped for raw, mapped in place_names.items()):
        lines.append("// Place identifiers:")
        lines.extend(f"//   {raw} -> {mapped}" for raw, mapped in place_names.items())
    if metric_names:
        lines.append("// Guard metric constants:")
        lines.extend(f"//   {raw} -> {mapped}" for raw, mapped in metric_names.items())
        lines.extend(f"const double {mapped};" for mapped in metric_names.values())
    lines.extend(["", f"module {module_identifier}"])

    for raw_name, identifier in place_names.items():
        initial = initial_marking[raw_name]
        if initial > token_bound:
            raise PolicyError(
                f"initial marking for {raw_name!r} exceeds max_tokens={token_bound}"
            )
        lines.append(f"  {identifier} : [0..{token_bound}] init {initial};")

    lines.append("")
    for transition in net.transitions:
        guard = _guard_expr(transition, metric_names)
        inputs = _input_expr(transition, place_names)
        command_guard = f"{guard} & {inputs}"
        update = _update_expr(transition, place_names)
        action = transition_names[transition.name]
        lines.append(f"  [{action}] {command_guard} -> {update};")

    lines.extend(["endmodule", ""])
    for raw_name, identifier in place_names.items():
        lines.append(f'label "active_{identifier}" = {identifier} > 0;')
        if raw_name != identifier:
            lines.append(f"// active_{identifier} maps original place {raw_name!r}")

    return PrismExport(
        model="\n".join(lines) + "\n",
        place_names=place_names,
        metric_names=metric_names,
        transition_names=transition_names,
    )

export_petri_net_tla

export_petri_net_tla(
    net: PetriNet,
    initial_marking: Marking,
    *,
    module_name: str = "SpoPetri",
    max_tokens: int | None = None,
) -> TLAExport

Serialise a Petri net into a bounded TLA+ transition-system module.

Guard metrics become TLA+ constants. Places become bounded natural-number variables, and each Petri transition becomes a named next-state action that preserves all unaffected places explicitly.

Parameters

net : PetriNet The Petri net to export. initial_marking : Marking The initial Petri net marking. module_name : str Name of the emitted model-checker module. max_tokens : int | None Maximum token bound per place, or None.

Returns

TLAExport The bounded TLA+ export of the Petri net.

Raises

PolicyError If the net or bounds violate the export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/petri_export.py
def export_petri_net_tla(
    net: PetriNet,
    initial_marking: Marking,
    *,
    module_name: str = "SpoPetri",
    max_tokens: int | None = None,
) -> TLAExport:
    """Serialise a Petri net into a bounded TLA+ transition-system module.

    Guard metrics become TLA+ constants. Places become bounded natural-number
    variables, and each Petri transition becomes a named next-state action that
    preserves all unaffected places explicitly.

    Parameters
    ----------
    net : PetriNet
        The Petri net to export.
    initial_marking : Marking
        The initial Petri net marking.
    module_name : str
        Name of the emitted model-checker module.
    max_tokens : int | None
        Maximum token bound per place, or ``None``.

    Returns
    -------
    TLAExport
        The bounded TLA+ export of the Petri net.

    Raises
    ------
    PolicyError
        If the net or bounds violate the export policy.
    """
    if not net.place_names:
        raise PolicyError("cannot export Petri net without places")
    if max_tokens is not None and max_tokens < 1:
        raise PolicyError("max_tokens must be >= 1")

    place_names = _place_mapping(net)
    transition_names = _transition_mapping(net)
    metric_names = _metric_mapping(net)
    token_bound = max_tokens or _token_upper_bound(net, initial_marking, minimum=1)
    module_identifier = _tla_module_identifier(module_name)
    variables = list(place_names.values())
    variable_tuple = "<<" + ", ".join(variables) + ">>"

    lines = [
        f"---- MODULE {module_identifier} ----",
        "EXTENDS Naturals, TLC",
        "",
        "\\* Generated from SCPN PetriNet for TLA+ model checking.",
    ]
    if any(raw != mapped for raw, mapped in place_names.items()):
        lines.append("\\* Place identifiers:")
        lines.extend(f"\\*   {raw} -> {mapped}" for raw, mapped in place_names.items())
    if metric_names:
        lines.append("\\* Guard metric constants:")
        lines.extend(f"\\*   {raw} -> {mapped}" for raw, mapped in metric_names.items())
        lines.append("CONSTANTS " + ", ".join(metric_names.values()))
    lines.extend(["", "VARIABLES " + ", ".join(variables), "", "Init =="])

    for raw_name, identifier in place_names.items():
        initial = initial_marking[raw_name]
        if initial > token_bound:
            raise PolicyError(
                f"initial marking for {raw_name!r} exceeds max_tokens={token_bound}"
            )
        lines.append(f"  /\\ {identifier} = {initial}")

    lines.extend(["", "TypeOK =="])
    lines.extend(
        f"  /\\ {identifier} \\in 0..{token_bound}" for identifier in variables
    )

    for transition in net.transitions:
        action = transition_names[transition.name]
        lines.extend(["", f"{action} =="])
        guard = _tla_guard_expr(transition, metric_names)
        if guard != "true":
            lines.append(f"  /\\ {guard}")
        inputs = _tla_input_expr(transition, place_names)
        if inputs != "TRUE":
            lines.append(f"  /\\ {inputs}")
        lines.extend(_tla_next_value_lines(transition, place_names))

    next_terms = [transition_names[transition.name] for transition in net.transitions]
    lines.extend(["", "Next =="])
    if next_terms:
        first, *rest = next_terms
        lines.append(f"  \\/ {first}")
        lines.extend(f"  \\/ {term}" for term in rest)
    else:
        lines.append("  /\\ UNCHANGED " + variable_tuple)

    lines.extend(
        [
            "",
            f"Spec == Init /\\ [][Next]_{variable_tuple}",
            "Safety == TypeOK",
            "",
        ]
    )
    for raw_name, identifier in place_names.items():
        lines.append(f"Active_{identifier} == {identifier} > 0")
        if raw_name != identifier:
            lines.append(f"\\* Active_{identifier} maps original place {raw_name!r}")
    lines.append("====")

    return TLAExport(
        module="\n".join(lines) + "\n",
        place_names=place_names,
        metric_names=metric_names,
        transition_names=transition_names,
    )

export_policy_rules_prism

export_policy_rules_prism(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> PrismExport

Serialise policy rules into a bounded PRISM MDP model.

Metrics and current regime are model inputs represented as PRISM constants. Each rule has a bounded fire counter; unlimited rules are represented as one-shot reachability counters for model-checking queries.

Parameters

rules : list[PolicyRule] The policy rules to export or validate. module_name : str Name of the emitted model-checker module.

Returns

PrismExport The bounded PRISM MDP export of the policy rules.

Raises

PolicyError If the rules violate the export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/policy_export.py
def export_policy_rules_prism(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> PrismExport:
    """Serialise policy rules into a bounded PRISM MDP model.

    Metrics and current regime are model inputs represented as PRISM
    constants. Each rule has a bounded fire counter; unlimited rules are
    represented as one-shot reachability counters for model-checking queries.

    Parameters
    ----------
    rules : list[PolicyRule]
        The policy rules to export or validate.
    module_name : str
        Name of the emitted model-checker module.

    Returns
    -------
    PrismExport
        The bounded PRISM MDP export of the policy rules.

    Raises
    ------
    PolicyError
        If the rules violate the export policy.
    """
    _validate_policy_rules_for_export(rules)

    metric_names = _policy_metric_mapping(rules)
    rule_names = _rule_mapping(rules)
    action_names = _action_mapping(rules)
    regime_names = _regime_mapping(rules)
    module_identifier = _identifier(module_name, prefix="module")

    lines = [
        "mdp",
        "",
        "// Generated from SCPN PolicyEngine rules for PRISM model checking.",
        "// Regime constants:",
    ]
    lines.extend(f"//   {name} -> {value}" for name, value in regime_names.items())
    lines.append(f"const int regime; // 0..{max(regime_names.values())}")
    if metric_names:
        lines.append("// Policy metric constants:")
        lines.extend(f"//   {raw} -> {mapped}" for raw, mapped in metric_names.items())
        lines.extend(f"const double {mapped};" for mapped in metric_names.values())
    lines.extend(["", f"module {module_identifier}"])

    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f"  {rule_id}_fires : [0..{_policy_fire_bound(rule)}] init 0;")

    lines.append("")
    for rule in rules:
        rule_id = rule_names[rule.name]
        guard = " & ".join(
            [
                _regime_guard_expr(rule, regime_names),
                _policy_guard_expr(rule.condition, metric_names),
                f"{rule_id}_fires < {_policy_fire_bound(rule)}",
            ]
        )
        lines.append(f"  [{rule_id}] {guard} -> ({rule_id}_fires'={rule_id}_fires+1);")

    lines.extend(["endmodule", ""])
    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f'label "fires_{rule_id}" = {rule_id}_fires > 0;')
        for i, action in enumerate(rule.actions):
            action_id = action_names[_action_key(rule, i)]
            lines.append(f'label "emits_{action_id}" = {rule_id}_fires > 0;')
            lines.append(
                f"//   {action_id}: knob={action.knob!r}, "
                f"scope={action.scope!r}, value={action.value:.17g}, "
                f"ttl_s={action.ttl_s:.17g}"
            )

    return PrismExport(
        model="\n".join(lines) + "\n",
        place_names={},
        metric_names=metric_names,
        transition_names={},
        rule_names=rule_names,
        action_names=action_names,
    )

export_policy_rules_tla

export_policy_rules_tla(
    rules: list[PolicyRule],
    *,
    module_name: str = "SpoPolicy",
) -> TLAExport

Serialise policy rules into a bounded TLA+ transition-system module.

Parameters

rules : list[PolicyRule] The policy rules to export or validate. module_name : str Name of the emitted model-checker module.

Returns

TLAExport The bounded TLA+ export of the policy rules.

Raises

PolicyError If the rules violate the export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/policy_export.py
def export_policy_rules_tla(
    rules: list[PolicyRule],
    *,
    module_name: str = "SpoPolicy",
) -> TLAExport:
    """Serialise policy rules into a bounded TLA+ transition-system module.

    Parameters
    ----------
    rules : list[PolicyRule]
        The policy rules to export or validate.
    module_name : str
        Name of the emitted model-checker module.

    Returns
    -------
    TLAExport
        The bounded TLA+ export of the policy rules.

    Raises
    ------
    PolicyError
        If the rules violate the export policy.
    """
    _validate_policy_rules_for_export(rules)

    metric_names = _policy_metric_mapping(rules)
    rule_names = _rule_mapping(rules)
    action_names = _action_mapping(rules)
    regime_names = _regime_mapping(rules)
    module_identifier = _tla_module_identifier(module_name)
    counters = [f"{rule_id}_fires" for rule_id in rule_names.values()]
    counter_tuple = "<<" + ", ".join(counters) + ">>"

    lines = [
        f"---- MODULE {module_identifier} ----",
        "EXTENDS Naturals, TLC",
        "",
        "\\* Generated from SCPN PolicyEngine rules for TLA+ model checking.",
        "\\* Regime constants:",
    ]
    lines.extend(f"\\*   {name} -> {value}" for name, value in regime_names.items())
    constants = ["regime", *metric_names.values()]
    lines.append("CONSTANTS " + ", ".join(constants))
    if metric_names:
        lines.append("\\* Policy metric constants:")
        lines.extend(f"\\*   {raw} -> {mapped}" for raw, mapped in metric_names.items())
    lines.extend(["", "VARIABLES " + ", ".join(counters), "", "Init =="])
    lines.extend(f"  /\\ {counter} = 0" for counter in counters)
    lines.extend(["", "TypeOK =="])
    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f"  /\\ {rule_id}_fires \\in 0..{_policy_fire_bound(rule)}")

    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.extend(["", f"{rule_id} =="])
        lines.append(f"  /\\ {_tla_regime_guard_expr(rule, regime_names)}")
        lines.append(f"  /\\ {_tla_policy_guard_expr(rule.condition, metric_names)}")
        lines.append(f"  /\\ {rule_id}_fires < {_policy_fire_bound(rule)}")
        lines.append(f"  /\\ {rule_id}_fires' = {rule_id}_fires + 1")
        lines.extend(_tla_unchanged_counter_lines(rule_id, rule_names))

    next_terms = list(rule_names.values())
    lines.extend(["", "Next =="])
    first, *rest = next_terms
    lines.append(f"  \\/ {first}")
    lines.extend(f"  \\/ {term}" for term in rest)

    lines.extend(
        [
            "",
            f"Spec == Init /\\ [][Next]_{counter_tuple}",
            "Safety == TypeOK",
            "",
        ]
    )
    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f"Fires_{rule_id} == {rule_id}_fires > 0")
        for i, action in enumerate(rule.actions):
            action_id = action_names[_action_key(rule, i)]
            lines.append(f"Emits_{action_id} == {rule_id}_fires > 0")
            lines.append(
                f"\\*   {action_id}: knob={action.knob!r}, "
                f"scope={action.scope!r}, value={action.value:.17g}, "
                f"ttl_s={action.ttl_s:.17g}"
            )
    lines.append("====")

    return TLAExport(
        module="\n".join(lines) + "\n",
        place_names={},
        metric_names=metric_names,
        transition_names={},
        rule_names=rule_names,
        action_names=action_names,
    )

audit_formal_checker_availability

audit_formal_checker_availability(
    package: FormalVerificationPackage,
    *,
    executable_paths: Mapping[str, str | None]
    | None = None,
) -> tuple[FormalCheckerAvailability, ...]

Return non-executing external-checker readiness records.

The audit resolves only the first command token for each package checker command. It never materialises artefacts, writes files, launches subprocesses, or changes the package execution policy. Tests and CI may inject executable_paths for deterministic readiness checks; production callers can omit it to use shutil.which against the current host.

Parameters

package : FormalVerificationPackage The formal verification package. executable_paths : Mapping[str, str | None] | None Mapping of checker name to executable path, or None.

Returns

tuple[FormalCheckerAvailability, ...] The non-executing external-checker readiness records.

Raises

PolicyError If the package fails its fail-closed policy checks.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def audit_formal_checker_availability(
    package: FormalVerificationPackage,
    *,
    executable_paths: Mapping[str, str | None] | None = None,
) -> tuple[FormalCheckerAvailability, ...]:
    """Return non-executing external-checker readiness records.

    The audit resolves only the first command token for each package checker
    command. It never materialises artefacts, writes files, launches subprocesses,
    or changes the package execution policy. Tests and CI may inject
    ``executable_paths`` for deterministic readiness checks; production callers
    can omit it to use ``shutil.which`` against the current host.

    Parameters
    ----------
    package : FormalVerificationPackage
        The formal verification package.
    executable_paths : Mapping[str, str | None] | None
        Mapping of checker name to executable path, or ``None``.

    Returns
    -------
    tuple[FormalCheckerAvailability, ...]
        The non-executing external-checker readiness records.

    Raises
    ------
    PolicyError
        If the package fails its fail-closed policy checks.
    """
    if not isinstance(package, FormalVerificationPackage):
        raise PolicyError("checker availability audit requires a formal package")
    records: list[FormalCheckerAvailability] = []
    for command in package.checker_commands:
        executable = command.command[0]
        if executable_paths is None:
            resolved_path = shutil.which(executable)
        elif executable in executable_paths:
            resolved_path = executable_paths[executable]
        else:
            resolved_path = None
        available = resolved_path is not None
        records.append(
            FormalCheckerAvailability(
                property_name=command.property_name,
                checker=command.checker,
                artifact_name=command.artifact_name,
                executable=executable,
                command=command.command,
                available=available,
                resolved_path=resolved_path,
                status="ready_not_executed" if available else "missing_executable",
                execution_permitted=False,
            )
        )
    return tuple(records)

build_runtime_control_certificate

build_runtime_control_certificate(
    package: FormalVerificationPackage,
    checker_availability: Sequence[
        FormalCheckerAvailability
    ],
    checker_results: Sequence[FormalCheckerResult],
    runtime_bounds: Mapping[str, object],
    *,
    certificate_name: str = "spo-runtime-control-certificate",
) -> FormalRuntimeCertificate

Build a fail-closed runtime certificate from formal evidence.

The certificate is verified only when every required package property has a matching available checker and a passed external result bound to the current package hash. Missing, failed, stale, or unavailable evidence produces a blocked certificate. The returned record never permits actuation.

Parameters

package : FormalVerificationPackage The formal verification package. checker_availability : Sequence[FormalCheckerAvailability] External-checker readiness records. checker_results : Sequence[FormalCheckerResult] External-checker result records. runtime_bounds : Mapping[str, object] Finite runtime bounds for the certificate. certificate_name : str Name for the emitted certificate.

Returns

FormalRuntimeCertificate The fail-closed runtime control certificate.

Raises

PolicyError If the formal evidence fails the fail-closed policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/runtime_certificate.py
def build_runtime_control_certificate(
    package: FormalVerificationPackage,
    checker_availability: Sequence[FormalCheckerAvailability],
    checker_results: Sequence[FormalCheckerResult],
    runtime_bounds: Mapping[str, object],
    *,
    certificate_name: str = "spo-runtime-control-certificate",
) -> FormalRuntimeCertificate:
    """Build a fail-closed runtime certificate from formal evidence.

    The certificate is verified only when every required package property has a
    matching available checker and a passed external result bound to the current
    package hash. Missing, failed, stale, or unavailable evidence produces a
    blocked certificate. The returned record never permits actuation.

    Parameters
    ----------
    package : FormalVerificationPackage
        The formal verification package.
    checker_availability : Sequence[FormalCheckerAvailability]
        External-checker readiness records.
    checker_results : Sequence[FormalCheckerResult]
        External-checker result records.
    runtime_bounds : Mapping[str, object]
        Finite runtime bounds for the certificate.
    certificate_name : str
        Name for the emitted certificate.

    Returns
    -------
    FormalRuntimeCertificate
        The fail-closed runtime control certificate.

    Raises
    ------
    PolicyError
        If the formal evidence fails the fail-closed policy.
    """
    if not isinstance(package, FormalVerificationPackage):
        raise PolicyError("runtime certificate requires a formal package")
    _require_package_identifier(certificate_name, "certificate_name")
    parsed_bounds = _validate_runtime_bounds(runtime_bounds)

    property_by_name = {property_.name: property_ for property_ in package.properties}
    availability_by_property: dict[str, FormalCheckerAvailability] = {}
    for record in checker_availability:
        if not isinstance(record, FormalCheckerAvailability):
            raise PolicyError("checker availability records are required")
        if record.property_name not in property_by_name:
            raise PolicyError("checker availability references unknown property")
        if record.property_name in availability_by_property:
            raise PolicyError("duplicate checker availability property")
        expected = property_by_name[record.property_name]
        if record.checker != expected.checker or record.artifact_name != (
            expected.artifact_name
        ):
            raise PolicyError("checker availability does not match package property")
        availability_by_property[record.property_name] = record

    result_by_property: dict[str, FormalCheckerResult] = {}
    for result in checker_results:
        if not isinstance(result, FormalCheckerResult):
            raise PolicyError("checker result records are required")
        if result.property_name not in property_by_name:
            raise PolicyError("checker result references unknown property")
        if result.property_name in result_by_property:
            raise PolicyError("duplicate checker result property")
        expected = property_by_name[result.property_name]
        if result.checker != expected.checker or result.artifact_name != (
            expected.artifact_name
        ):
            raise PolicyError("checker result does not match package property")
        if result.package_hash != package.package_hash:
            raise PolicyError("checker result package_hash does not match package")
        result_by_property[result.property_name] = result

    required_names = tuple(
        property_.name for property_ in package.properties if property_.required
    )
    missing_required = tuple(
        name for name in required_names if name not in result_by_property
    )
    failed_required = tuple(
        name
        for name in required_names
        if name in result_by_property and not result_by_property[name].passed
    )
    unavailable_required = tuple(
        name
        for name in required_names
        if name not in availability_by_property
        or not availability_by_property[name].available
    )
    passed_required_count = sum(
        int(
            name in result_by_property
            and result_by_property[name].passed
            and name in availability_by_property
            and availability_by_property[name].available
        )
        for name in required_names
    )
    status = (
        "verified_non_actuating"
        if not missing_required and not failed_required and not unavailable_required
        else "blocked"
    )
    sorted_availability = tuple(
        availability_by_property[name] for name in sorted(availability_by_property)
    )
    sorted_results = tuple(
        result_by_property[name] for name in sorted(result_by_property)
    )
    certificate_seed = {
        "certificate_name": certificate_name,
        "package_name": package.package_name,
        "package_hash": package.package_hash,
        "runtime_bounds": dict(sorted(parsed_bounds.items())),
        "checker_availability": [
            item.to_audit_record() for item in sorted_availability
        ],
        "checker_results": [item.to_audit_record() for item in sorted_results],
        "required_property_count": len(required_names),
        "passed_required_count": passed_required_count,
        "missing_required_properties": list(missing_required),
        "failed_required_properties": list(failed_required),
        "unavailable_checker_properties": list(unavailable_required),
        "status": status,
        "actuation_permitted": False,
    }
    certificate_hash = hashlib.sha256(
        json.dumps(
            certificate_seed,
            sort_keys=True,
            separators=(",", ":"),
        ).encode("utf-8")
    ).hexdigest()
    return FormalRuntimeCertificate(
        certificate_name=certificate_name,
        package_name=package.package_name,
        package_hash=package.package_hash,
        runtime_bounds=parsed_bounds,
        checker_availability=sorted_availability,
        checker_results=sorted_results,
        required_property_count=len(required_names),
        passed_required_count=passed_required_count,
        missing_required_properties=tuple(sorted(missing_required)),
        failed_required_properties=tuple(sorted(failed_required)),
        unavailable_checker_properties=tuple(sorted(unavailable_required)),
        status=status,
        certificate_hash=certificate_hash,
        actuation_permitted=False,
    )

export_policy_rules_smt

export_policy_rules_smt(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> FormalTextArtifact

Serialise policy rules into a bounded SMT-LIB feasibility model.

The export declares the active regime, metric inputs, bounded rule-fire counters, rule firing predicates, and action emission predicates. The final assertion asks an SMT solver whether at least one policy rule can fire under the declared constraints. The function only generates deterministic text; it does not invoke Z3 or any other solver.

Parameters

rules : list[PolicyRule] The policy rules to export or validate. module_name : str Name recorded in the emitted SMT-LIB comments.

Returns

FormalTextArtifact An SMT-LIB v2 artifact suitable for package hashing and Z3 execution.

Raises

PolicyError If the rules violate the shared formal-export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/smt_export.py
def export_policy_rules_smt(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> FormalTextArtifact:
    """Serialise policy rules into a bounded SMT-LIB feasibility model.

    The export declares the active regime, metric inputs, bounded rule-fire
    counters, rule firing predicates, and action emission predicates. The final
    assertion asks an SMT solver whether at least one policy rule can fire under
    the declared constraints. The function only generates deterministic text; it
    does not invoke Z3 or any other solver.

    Parameters
    ----------
    rules : list[PolicyRule]
        The policy rules to export or validate.
    module_name : str
        Name recorded in the emitted SMT-LIB comments.

    Returns
    -------
    FormalTextArtifact
        An SMT-LIB v2 artifact suitable for package hashing and Z3 execution.

    Raises
    ------
    PolicyError
        If the rules violate the shared formal-export policy.
    """
    _validate_policy_rules_for_export(rules)

    metric_names = _policy_metric_mapping(rules)
    rule_names = _rule_mapping(rules)
    action_names = _action_mapping(rules)
    regime_names = _regime_mapping(rules)
    module_identifier = _identifier(module_name, prefix="module")

    lines = [
        "(set-logic QF_LRA)",
        "; Generated from SCPN PolicyEngine rules for SMT feasibility checking.",
        f"; Module: {module_identifier}",
        "; Regime constants:",
    ]
    lines.extend(f";   {name} -> {value}" for name, value in regime_names.items())
    lines.extend(["", "(declare-const regime Real)"])
    lines.extend(
        f"(declare-const {metric_id} Real)" for metric_id in metric_names.values()
    )

    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f"(declare-const {rule_id}_fire_count Real)")

    valid_regimes = " ".join(
        f"(= regime {value})" for value in sorted(regime_names.values())
    )
    lines.extend(["", f"(assert (or {valid_regimes}))"])

    for rule in rules:
        rule_id = rule_names[rule.name]
        bound = _policy_fire_bound(rule)
        lines.append(f"(assert (>= {rule_id}_fire_count 0))")
        lines.append(f"(assert (<= {rule_id}_fire_count {bound}))")

    lines.append("")
    if metric_names:
        lines.append("; Policy metric mapping:")
        lines.extend(f";   {raw} -> {mapped}" for raw, mapped in metric_names.items())
        lines.append("")

    firing_terms: list[str] = []
    for rule in rules:
        rule_id = rule_names[rule.name]
        firing_terms.append(f"fires_{rule_id}")
        lines.append(f"; Rule {rule.name!r} -> {rule_id}")
        lines.append(
            f"(define-fun fires_{rule_id} () Bool "
            f"(and {_smt_regime_guard_expr(rule, regime_names)} "
            f"{_smt_policy_guard_expr(rule.condition, metric_names)} "
            f"(< {rule_id}_fire_count {_policy_fire_bound(rule)})))"
        )
        for action_index, action in enumerate(rule.actions):
            action_id = action_names[_action_key(rule, action_index)]
            lines.append(f"(define-fun emits_{action_id} () Bool fires_{rule_id})")
            lines.append(
                f";   {action_id}: knob={action.knob!r}, "
                f"scope={action.scope!r}, value={action.value:.17g}, "
                f"ttl_s={action.ttl_s:.17g}"
            )
        lines.append("")

    if len(firing_terms) == 1:
        lines.append(f"(assert {firing_terms[0]})")
    else:
        lines.append(f"(assert (or {' '.join(firing_terms)}))")
    lines.append("(check-sat)")

    return FormalTextArtifact(
        artifact_type="smt2",
        text="\n".join(lines) + "\n",
    )

export_stl_specs_prism

export_stl_specs_prism(
    specs: list[PolicySTLSpec],
    *,
    module_name: str = "spo_stl",
) -> PrismExport

Serialise policy-declared STL monitors into PRISM label surfaces.

This export covers the builtin STL subset used by STLMonitor: always (...) and eventually (...) over numeric predicate conjunctions. The model is a single-state abstraction with signal constants and per-monitor satisfied/violated labels for property checks.

Parameters

specs : list[PolicySTLSpec] Policy-declared STL monitor specifications. module_name : str Name of the emitted model-checker module.

Returns

PrismExport The PRISM label-surface export of the STL monitors.

Raises

PolicyError If the STL specs violate the export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/stl_export.py
def export_stl_specs_prism(
    specs: list[PolicySTLSpec],
    *,
    module_name: str = "spo_stl",
) -> PrismExport:
    """Serialise policy-declared STL monitors into PRISM label surfaces.

    This export covers the builtin STL subset used by ``STLMonitor``:
    ``always (...)`` and ``eventually (...)`` over numeric predicate
    conjunctions. The model is a single-state abstraction with signal
    constants and per-monitor satisfied/violated labels for property checks.

    Parameters
    ----------
    specs : list[PolicySTLSpec]
        Policy-declared STL monitor specifications.
    module_name : str
        Name of the emitted model-checker module.

    Returns
    -------
    PrismExport
        The PRISM label-surface export of the STL monitors.

    Raises
    ------
    PolicyError
        If the STL specs violate the export policy.
    """
    if not specs:
        raise PolicyError("cannot export STL monitors without specs")
    for spec in specs:
        if not spec.name:
            raise PolicyError("STL monitor names must not be empty")
        if spec.severity not in {"soft", "hard"}:
            raise PolicyError(f"STL monitor {spec.name!r} has unsupported severity")

    stl_names = _stl_mapping(specs)
    signal_names, parsed = _stl_signal_mapping(specs)
    module_identifier = _identifier(module_name, prefix="module")

    lines = [
        "mdp",
        "",
        "// Generated from SCPN policy STL monitors for PRISM model checking.",
        "// Signal constants represent one sampled trace point or scenario bound.",
    ]
    if signal_names:
        lines.append("// STL signal constants:")
        lines.extend(f"//   {raw} -> {mapped}" for raw, mapped in signal_names.items())
        lines.extend(f"const double {mapped};" for mapped in signal_names.values())
    lines.extend(
        [
            "",
            f"module {module_identifier}",
            "  state : [0..0] init 0;",
            "endmodule",
            "",
        ]
    )

    for spec in specs:
        stl_id = stl_names[spec.name]
        temporal_op, predicates = parsed[spec.name]
        expr = _stl_expr(predicates, signal_names)
        lines.append(
            f"// STL {spec.name!r}: {temporal_op} monitor, severity={spec.severity}"
        )
        lines.append(f'label "stl_{stl_id}_satisfied" = {expr};')
        lines.append(f'label "stl_{stl_id}_violated" = !({expr});')

    return PrismExport(
        model="\n".join(lines) + "\n",
        place_names={},
        metric_names=signal_names,
        transition_names={},
        stl_names=stl_names,
    )

build_formal_verification_package

build_formal_verification_package(
    artifacts: Mapping[
        str, PrismExport | TLAExport | FormalTextArtifact
    ],
    properties: Sequence[FormalSafetyProperty],
    *,
    package_name: str = "spo-formal-verification",
) -> FormalVerificationPackage

Build a deterministic manifest for external model-checker execution.

The package records exported artefact hashes, property-library entries, and exact checker commands. It never writes files or invokes external tools; CI or operators can materialise the package and run the recorded commands in a controlled environment.

Parameters

artifacts : Mapping[str, PrismExport | TLAExport | FormalTextArtifact] Mapping of artefact name to its formal export. properties : Sequence[FormalSafetyProperty] The formal safety properties. package_name : str Name for the verification package.

Returns

FormalVerificationPackage The formal verification package manifest.

Raises

PolicyError If the artefacts or properties fail policy checks.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/verification_package.py
def build_formal_verification_package(
    artifacts: Mapping[str, PrismExport | TLAExport | FormalTextArtifact],
    properties: Sequence[FormalSafetyProperty],
    *,
    package_name: str = "spo-formal-verification",
) -> FormalVerificationPackage:
    """Build a deterministic manifest for external model-checker execution.

    The package records exported artefact hashes, property-library entries, and
    exact checker commands. It never writes files or invokes external tools;
    CI or operators can materialise the package and run the recorded commands
    in a controlled environment.

    Parameters
    ----------
    artifacts : Mapping[str, PrismExport | TLAExport | FormalTextArtifact]
        Mapping of artefact name to its formal export.
    properties : Sequence[FormalSafetyProperty]
        The formal safety properties.
    package_name : str
        Name for the verification package.

    Returns
    -------
    FormalVerificationPackage
        The formal verification package manifest.

    Raises
    ------
    PolicyError
        If the artefacts or properties fail policy checks.
    """
    _require_package_identifier(package_name, "package_name")
    if not artifacts:
        raise PolicyError("formal verification package requires artifacts")
    if not properties:
        raise PolicyError("formal verification package requires properties")

    artifact_hashes: dict[str, str] = {}
    artifact_types: dict[str, str] = {}
    for artifact_name, export in sorted(artifacts.items()):
        _require_package_identifier(artifact_name, "artifact name")
        if not isinstance(export, PrismExport | TLAExport | FormalTextArtifact):
            raise PolicyError(
                "formal artifacts must be PrismExport, TLAExport, or FormalTextArtifact"
            )
        artifact_text = _artifact_text(export)
        if not artifact_text.strip():
            raise PolicyError(f"formal artifact {artifact_name!r} is empty")
        artifact_hashes[artifact_name] = hashlib.sha256(
            artifact_text.encode("utf-8")
        ).hexdigest()
        artifact_types[artifact_name] = _artifact_type(export)

    property_names: set[str] = set()
    commands: list[FormalCheckerCommand] = []
    for property_ in properties:
        if property_.name in property_names:
            raise PolicyError(f"duplicate formal property {property_.name!r}")
        property_names.add(property_.name)
        if property_.artifact_name not in artifact_hashes:
            raise PolicyError(
                f"formal property {property_.name!r} references unknown artifact "
                f"{property_.artifact_name!r}"
            )
        if not _checker_matches_artifact(
            property_,
            artifact_types[property_.artifact_name],
        ):
            raise PolicyError(
                f"formal property {property_.name!r} checker does not match "
                f"artifact {property_.artifact_name!r}"
            )
        commands.append(_checker_command(property_))

    package_seed = {
        "package_name": package_name,
        "artifact_hashes": dict(sorted(artifact_hashes.items())),
        "artifact_types": dict(sorted(artifact_types.items())),
        "properties": [item.to_audit_record() for item in properties],
        "checker_commands": [command.to_audit_record() for command in commands],
    }
    package_hash = hashlib.sha256(
        json.dumps(package_seed, sort_keys=True, separators=(",", ":")).encode("utf-8")
    ).hexdigest()
    return FormalVerificationPackage(
        package_name=package_name,
        artifact_hashes=artifact_hashes,
        artifact_types=artifact_types,
        properties=tuple(properties),
        checker_commands=tuple(commands),
        package_hash=package_hash,
    )

smt_export

SMT-LIB text exporter for bounded supervisor policy rule feasibility.

Classes

Functions:

export_policy_rules_smt

export_policy_rules_smt(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> FormalTextArtifact

Serialise policy rules into a bounded SMT-LIB feasibility model.

The export declares the active regime, metric inputs, bounded rule-fire counters, rule firing predicates, and action emission predicates. The final assertion asks an SMT solver whether at least one policy rule can fire under the declared constraints. The function only generates deterministic text; it does not invoke Z3 or any other solver.

Parameters

rules : list[PolicyRule] The policy rules to export or validate. module_name : str Name recorded in the emitted SMT-LIB comments.

Returns

FormalTextArtifact An SMT-LIB v2 artifact suitable for package hashing and Z3 execution.

Raises

PolicyError If the rules violate the shared formal-export policy.

Source code in src/scpn_phase_orchestrator/supervisor/formal_export/smt_export.py
def export_policy_rules_smt(
    rules: list[PolicyRule],
    *,
    module_name: str = "spo_policy",
) -> FormalTextArtifact:
    """Serialise policy rules into a bounded SMT-LIB feasibility model.

    The export declares the active regime, metric inputs, bounded rule-fire
    counters, rule firing predicates, and action emission predicates. The final
    assertion asks an SMT solver whether at least one policy rule can fire under
    the declared constraints. The function only generates deterministic text; it
    does not invoke Z3 or any other solver.

    Parameters
    ----------
    rules : list[PolicyRule]
        The policy rules to export or validate.
    module_name : str
        Name recorded in the emitted SMT-LIB comments.

    Returns
    -------
    FormalTextArtifact
        An SMT-LIB v2 artifact suitable for package hashing and Z3 execution.

    Raises
    ------
    PolicyError
        If the rules violate the shared formal-export policy.
    """
    _validate_policy_rules_for_export(rules)

    metric_names = _policy_metric_mapping(rules)
    rule_names = _rule_mapping(rules)
    action_names = _action_mapping(rules)
    regime_names = _regime_mapping(rules)
    module_identifier = _identifier(module_name, prefix="module")

    lines = [
        "(set-logic QF_LRA)",
        "; Generated from SCPN PolicyEngine rules for SMT feasibility checking.",
        f"; Module: {module_identifier}",
        "; Regime constants:",
    ]
    lines.extend(f";   {name} -> {value}" for name, value in regime_names.items())
    lines.extend(["", "(declare-const regime Real)"])
    lines.extend(
        f"(declare-const {metric_id} Real)" for metric_id in metric_names.values()
    )

    for rule in rules:
        rule_id = rule_names[rule.name]
        lines.append(f"(declare-const {rule_id}_fire_count Real)")

    valid_regimes = " ".join(
        f"(= regime {value})" for value in sorted(regime_names.values())
    )
    lines.extend(["", f"(assert (or {valid_regimes}))"])

    for rule in rules:
        rule_id = rule_names[rule.name]
        bound = _policy_fire_bound(rule)
        lines.append(f"(assert (>= {rule_id}_fire_count 0))")
        lines.append(f"(assert (<= {rule_id}_fire_count {bound}))")

    lines.append("")
    if metric_names:
        lines.append("; Policy metric mapping:")
        lines.extend(f";   {raw} -> {mapped}" for raw, mapped in metric_names.items())
        lines.append("")

    firing_terms: list[str] = []
    for rule in rules:
        rule_id = rule_names[rule.name]
        firing_terms.append(f"fires_{rule_id}")
        lines.append(f"; Rule {rule.name!r} -> {rule_id}")
        lines.append(
            f"(define-fun fires_{rule_id} () Bool "
            f"(and {_smt_regime_guard_expr(rule, regime_names)} "
            f"{_smt_policy_guard_expr(rule.condition, metric_names)} "
            f"(< {rule_id}_fire_count {_policy_fire_bound(rule)})))"
        )
        for action_index, action in enumerate(rule.actions):
            action_id = action_names[_action_key(rule, action_index)]
            lines.append(f"(define-fun emits_{action_id} () Bool fires_{rule_id})")
            lines.append(
                f";   {action_id}: knob={action.knob!r}, "
                f"scope={action.scope!r}, value={action.value:.17g}, "
                f"ttl_s={action.ttl_s:.17g}"
            )
        lines.append("")

    if len(firing_terms) == 1:
        lines.append(f"(assert {firing_terms[0]})")
    else:
        lines.append(f"(assert (or {' '.join(firing_terms)}))")
    lines.append("(check-sat)")

    return FormalTextArtifact(
        artifact_type="smt2",
        text="\n".join(lines) + "\n",
    )

export_petri_net_to_prism renders a guard-gated Petri net as a finite PRISM MDP model, preserving the runtime engine's first-enabled transition priority and exposing guard metrics as PRISM constants for scenario binding.

formal

Formal verification exporters for guard-gated Petri nets.

Classes

Functions:

export_petri_net_to_prism

export_petri_net_to_prism(
    net: PetriNet,
    initial: Marking,
    *,
    module_name: str = "supervisor",
    max_tokens: int | None = None,
    include_idle: bool = True,
) -> str

Export a guard-gated Petri net as a finite PRISM MDP model.

The exporter preserves the runtime engine's first-enabled transition priority by blocking each command when an earlier transition is enabled. Guard metrics become PRISM constants so verification jobs can bind them explicitly for a scenario.

Parameters

net : PetriNet Guard-gated Petri net to export; its transitions define the commands and their first-enabled priority order. initial : Marking Initial token count for each place. module_name : str Name of the generated PRISM module; sanitised to a valid identifier. max_tokens : int or None Upper bound on tokens per place. When None, a bound is derived from the net structure and the initial marking. include_idle : bool When True, emit an [idle] self-loop that fires only when no transition is enabled, keeping the MDP deadlock-free.

Returns

str The PRISM MDP model source.

Raises

PolicyError If max_tokens is less than 1, or if an initial marking exceeds the token bound.

Source code in src/scpn_phase_orchestrator/supervisor/formal.py
def export_petri_net_to_prism(
    net: PetriNet,
    initial: Marking,
    *,
    module_name: str = "supervisor",
    max_tokens: int | None = None,
    include_idle: bool = True,
) -> str:
    """Export a guard-gated Petri net as a finite PRISM MDP model.

    The exporter preserves the runtime engine's first-enabled transition
    priority by blocking each command when an earlier transition is enabled.
    Guard metrics become PRISM constants so verification jobs can bind them
    explicitly for a scenario.

    Parameters
    ----------
    net : PetriNet
        Guard-gated Petri net to export; its transitions define the commands
        and their first-enabled priority order.
    initial : Marking
        Initial token count for each place.
    module_name : str
        Name of the generated PRISM module; sanitised to a valid identifier.
    max_tokens : int or None
        Upper bound on tokens per place. When ``None``, a bound is derived from
        the net structure and the initial marking.
    include_idle : bool
        When ``True``, emit an ``[idle]`` self-loop that fires only when no
        transition is enabled, keeping the MDP deadlock-free.

    Returns
    -------
    str
        The PRISM MDP model source.

    Raises
    ------
    PolicyError
        If ``max_tokens`` is less than 1, or if an initial marking exceeds the
        token bound.
    """
    if max_tokens is not None and max_tokens < 1:
        raise PolicyError("max_tokens must be >= 1")

    place_names = sorted(net.place_names)
    place_ids = _identifier_map(place_names, prefix="p")
    transition_ids = _identifier_map(
        [transition.name for transition in net.transitions], prefix="t"
    )
    metric_ids = _identifier_map(_guard_metric_names(net.transitions), prefix="m")
    token_bound = max_tokens or _default_token_bound(net, initial)
    module_id = _safe_identifier(module_name, "module")

    lines = ["mdp", ""]
    for _metric, metric_id in metric_ids.items():
        lines.append(f"const double {metric_id};")
    if metric_ids:
        lines.append("")
    lines.append(f"module {module_id}")
    for place in place_names:
        init_count = initial[place]
        if init_count > token_bound:
            raise PolicyError(
                f"initial marking for {place!r} exceeds max_tokens={token_bound}"
            )
        lines.append(f"  {place_ids[place]} : [0..{token_bound}] init {init_count};")
    lines.append("")

    enabled_formulas: list[str] = []
    for transition in net.transitions:
        transition_id = transition_ids[transition.name]
        expr = _enabled_expr(transition, place_ids, metric_ids, token_bound)
        enabled_name = f"enabled_{transition_id}"
        enabled_formulas.append(enabled_name)
        lines.append(f"  formula {enabled_name} = {expr};")
    lines.append("")

    earlier: list[str] = []
    for transition in net.transitions:
        transition_id = transition_ids[transition.name]
        enabled_name = f"enabled_{transition_id}"
        priority_guard = enabled_name
        if earlier:
            priority_guard = f"{priority_guard} & !({' | '.join(earlier)})"
        update = _update_expr(transition, place_ids)
        lines.append(f"  [{transition_id}] {priority_guard} -> {update};")
        earlier.append(enabled_name)
    if include_idle:
        if enabled_formulas:
            lines.append(f"  [idle] !({' | '.join(enabled_formulas)}) -> true;")
        else:
            lines.append("  [idle] true -> true;")
    lines.append("endmodule")
    lines.append("")
    return "\n".join(lines)

Rust Supervisor Backend Probe

The Python supervisor remains the default runtime-control surface. The optional Rust spo-supervisor PyO3 bindings are validated separately through audit_rust_supervisor_backend(), which checks required spo_kernel symbols and runs deterministic, non-actuating smoke checks for regime classification, boundary observation, and coherence monitoring. spo doctor reports this as the optional rust-supervisor backend so operators can diagnose a missing or malformed Rust supervisor FFI without changing live-control behavior.

rust_backend

Optional Rust supervisor FFI readiness checks.

The live Python supervisor remains the default runtime-control path. This module answers a narrower packaging and operations question: when the optional spo_kernel wheel is installed, does it expose the spo-supervisor PyO3 surface and do the deterministic regime, boundary, and coherence primitives behave like usable supervisor components?

All probes fail closed. Missing symbols, malformed smoke outputs, or import failures produce an unavailable status and never mutate production supervisor state.

Classes

RustSupervisorBackendStatus dataclass

RustSupervisorBackendStatus(
    available: bool,
    symbols: tuple[str, ...],
    missing_symbols: tuple[str, ...],
    detail: str,
    smoke: Mapping[str, object] = dict(),
)

Readiness outcome for the optional Rust supervisor FFI backend.

Attributes
available: True only when the module imports, all required symbols are
    present, and deterministic smoke validation succeeds.
symbols: Required PyO3 symbols checked on the module.
missing_symbols: Required symbols absent from the inspected module.
detail: Human-facing readiness explanation for ``spo doctor`` and audit
    records.
smoke: Deterministic smoke observations when validation succeeds.
Attributes
status property
status: str

Return ok when available, otherwise warn.

Returns

str ok for a usable optional backend and warn for an unavailable optional backend.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-serialisable backend audit record.

Returns

dict[str, object] Backend readiness details suitable for doctor JSON output, release evidence, or internal handoff logs.

Source code in src/scpn_phase_orchestrator/supervisor/rust_backend.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-serialisable backend audit record.

    Returns
    -------
    dict[str, object]
        Backend readiness details suitable for doctor JSON output, release
        evidence, or internal handoff logs.
    """
    return {
        "backend": "rust-supervisor",
        "status": self.status,
        "available": self.available,
        "symbols": list(self.symbols),
        "missing_symbols": list(self.missing_symbols),
        "detail": self.detail,
        "smoke": dict(self.smoke),
    }

Functions:

audit_rust_supervisor_backend

audit_rust_supervisor_backend(
    module: object | None = None,
) -> RustSupervisorBackendStatus

Probe the optional spo_kernel supervisor FFI surface.

Parameters

module : object | None Optional module-like object used by tests. When None, spo_kernel is imported lazily.

Returns

RustSupervisorBackendStatus Fail-closed readiness status for the optional Rust supervisor backend.

Source code in src/scpn_phase_orchestrator/supervisor/rust_backend.py
def audit_rust_supervisor_backend(
    module: object | None = None,
) -> RustSupervisorBackendStatus:
    """Probe the optional ``spo_kernel`` supervisor FFI surface.

    Parameters
    ----------
    module : object | None
        Optional module-like object used by tests. When ``None``, ``spo_kernel``
        is imported lazily.

    Returns
    -------
    RustSupervisorBackendStatus
        Fail-closed readiness status for the optional Rust supervisor backend.
    """
    if module is None:
        try:
            module = importlib.import_module("spo_kernel")
        except Exception as exc:
            return RustSupervisorBackendStatus(
                available=False,
                symbols=SUPERVISOR_RUST_SYMBOLS,
                missing_symbols=SUPERVISOR_RUST_SYMBOLS,
                detail=f"spo_kernel not importable: {type(exc).__name__}",
            )

    missing = tuple(
        symbol for symbol in SUPERVISOR_RUST_SYMBOLS if not hasattr(module, symbol)
    )
    if missing:
        return RustSupervisorBackendStatus(
            available=False,
            symbols=SUPERVISOR_RUST_SYMBOLS,
            missing_symbols=missing,
            detail=f"missing supervisor FFI symbols: {', '.join(missing)}",
        )

    try:
        smoke = _run_supervisor_smoke(module)
    except (AttributeError, KeyError, TypeError, ValueError) as exc:
        return RustSupervisorBackendStatus(
            available=False,
            symbols=SUPERVISOR_RUST_SYMBOLS,
            missing_symbols=(),
            detail=f"supervisor FFI smoke failed: {exc}",
        )

    return RustSupervisorBackendStatus(
        available=True,
        symbols=SUPERVISOR_RUST_SYMBOLS,
        missing_symbols=(),
        detail="spo-supervisor FFI symbols and deterministic smoke checks passed",
        smoke=smoke,
    )

Petri Net FSM

Formal Petri net state machine enabling formal verification of safety properties: deadlock freedom, liveness, bounded token counts.

Components

Class Fields Description
Place name: str Token container (regime state)
Arc place: str, weight: int Token flow edge
Guard metric: str, op: str, threshold: float Firing condition
Transition name, inputs, outputs, guard Guarded state change
Marking tokens: dict[str, int] Current token distribution

Guard operators

Guards support five comparison operators: >, >=, <, <=, ==. Guard.evaluate(ctx) checks the condition against a context dictionary.

PetriNet methods

Method Description
enabled(marking, ctx) Returns transitions whose guards pass
fire(marking, transition) Moves tokens and returns new marking
step(marking, ctx) Fires first enabled transition

parse_guard("R < 0.3") parses a string into a Guard object.

Performance: enabled_transitions() < 10 μs.

petri_net

Guarded Petri-net primitives for deterministic regime transition modeling.

The module defines validated places, weighted arcs, guards, transitions, markings, and a first-match-priority Petri net. Marking updates are local and non-negative, guard metrics must be finite, and net construction rejects arcs to unknown places. The engine performs no event emission or policy action mapping; adapter modules own those boundaries.

Classes

Place dataclass

Place(name: str)

Named place (state) in the Petri net.

Arc dataclass

Arc(place: str, weight: int = 1)

Weighted arc connecting a place to a transition.

Guard dataclass

Guard(metric: str, op: str, threshold: float)

Boolean guard condition on a named metric (e.g. 'stability_proxy > 0.6').

Methods:
evaluate
evaluate(ctx: Mapping[str, float]) -> bool

Return True if the guard condition is satisfied by ctx.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def evaluate(self, ctx: Mapping[str, float]) -> bool:
    """Return True if the guard condition is satisfied by *ctx*."""
    val = ctx.get(self.metric)
    if val is None:
        return False
    val = _validate_finite_real(val, name=f"context metric {self.metric!r}")
    fn = _OPS.get(self.op)
    if fn is None:
        return False
    return bool(fn(val, self.threshold))

Transition dataclass

Transition(
    name: str,
    inputs: list[Arc],
    outputs: list[Arc],
    guard: Guard | None = None,
)

Petri net transition with input/output arcs and optional guard.

Marking dataclass

Marking(tokens: dict[str, int] = dict())

Token distribution across places in a Petri net.

Methods:
active_places
active_places() -> list[str]

Return names of places that hold at least one token.

Returns

list[str] Return names of places that hold at least one token.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def active_places(self) -> list[str]:
    """Return names of places that hold at least one token.

    Returns
    -------
    list[str]
        Return names of places that hold at least one token.
    """
    return [p for p, n in self.tokens.items() if n > 0]
copy
copy() -> Marking

Return a shallow copy of this marking.

Returns

Marking Return a shallow copy of this marking.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def copy(self) -> Marking:
    """Return a shallow copy of this marking.

    Returns
    -------
    Marking
        Return a shallow copy of this marking.
    """
    return Marking(tokens=dict(self.tokens))

PetriNet

PetriNet(
    places: list[Place], transitions: list[Transition]
)

Classical Petri net with guard-gated transitions.

step() fires at most one enabled transition per call (first-match priority).

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def __init__(
    self,
    places: list[Place],
    transitions: list[Transition],
) -> None:
    self._place_names = frozenset(p.name for p in places)
    self._transitions = transitions
    self._guard_metrics = frozenset(
        t.guard.metric for t in transitions if t.guard is not None
    )
    self._validate()
Attributes
place_names property
place_names: frozenset[str]

All place names registered in this net.

Returns

frozenset[str] All place names registered in this net.

transitions property
transitions: list[Transition]

All transitions in firing-priority order.

Returns

list[Transition] All transitions in firing-priority order.

guard_metrics property
guard_metrics: frozenset[str]

Whitelisted context metric names used by transition guards.

Returns

frozenset[str] Whitelisted context metric names used by transition guards.

Methods:
enabled
enabled(
    marking: Marking, ctx: Mapping[str, float]
) -> list[Transition]

Return all transitions whose input arcs and guards are satisfied.

Parameters

marking : Marking The Petri net marking (token distribution). ctx : Mapping[str, float] Context metric values keyed by guard-metric name.

Returns

list[Transition] The transitions whose input arcs and guards are satisfied.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def enabled(self, marking: Marking, ctx: Mapping[str, float]) -> list[Transition]:
    """Return all transitions whose input arcs and guards are satisfied.

    Parameters
    ----------
    marking : Marking
        The Petri net marking (token distribution).
    ctx : Mapping[str, float]
        Context metric values keyed by guard-metric name.

    Returns
    -------
    list[Transition]
        The transitions whose input arcs and guards are satisfied.
    """
    ctx = self._validated_context(ctx)
    result = []
    for t in self._transitions:
        if t.guard is not None and not t.guard.evaluate(ctx):
            continue
        if all(marking[arc.place] >= arc.weight for arc in t.inputs):
            result.append(t)
    return result
fire
fire(marking: Marking, transition: Transition) -> Marking

Fire transition, consuming input tokens and producing output tokens.

Parameters

marking : Marking The Petri net marking (token distribution). transition : Transition The transition to fire.

Returns

Marking The marking after firing the transition.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def fire(self, marking: Marking, transition: Transition) -> Marking:
    """Fire *transition*, consuming input tokens and producing output tokens.

    Parameters
    ----------
    marking : Marking
        The Petri net marking (token distribution).
    transition : Transition
        The transition to fire.

    Returns
    -------
    Marking
        The marking after firing the transition.
    """
    new = marking.copy()
    for arc in transition.inputs:
        new[arc.place] = new[arc.place] - arc.weight
    for arc in transition.outputs:
        new[arc.place] = new[arc.place] + arc.weight
    return new
step
step(
    marking: Marking, ctx: Mapping[str, float]
) -> tuple[Marking, Transition | None]

Fire the first enabled transition, return (new_marking, fired_transition).

Parameters

marking : Marking The Petri net marking (token distribution). ctx : Mapping[str, float] Context metric values keyed by guard-metric name.

Returns

tuple[Marking, Transition | None] The new marking and the fired transition (or None).

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def step(
    self, marking: Marking, ctx: Mapping[str, float]
) -> tuple[Marking, Transition | None]:
    """Fire the first enabled transition, return (new_marking, fired_transition).

    Parameters
    ----------
    marking : Marking
        The Petri net marking (token distribution).
    ctx : Mapping[str, float]
        Context metric values keyed by guard-metric name.

    Returns
    -------
    tuple[Marking, Transition | None]
        The new marking and the fired transition (or ``None``).
    """
    ctx = self._validated_context(ctx)
    for t in self._transitions:
        if t.guard is not None and not t.guard.evaluate(ctx):
            continue
        if all(marking[arc.place] >= arc.weight for arc in t.inputs):
            return self.fire(marking, t), t
    return marking, None

Functions:

parse_guard

parse_guard(text: str) -> Guard

Parse guard string like 'stability_proxy > 0.6'.

Source code in src/scpn_phase_orchestrator/supervisor/petri_net.py
def parse_guard(text: str) -> Guard:
    """Parse guard string like 'stability_proxy > 0.6'."""
    parts = text.split()
    if len(parts) != 3:
        raise PolicyError(f"guard must be 'metric op threshold', got {text!r}")
    try:
        threshold = float(parts[2])
    except ValueError as exc:
        raise PolicyError(f"threshold must be finite, got {parts[2]!r}") from exc
    return Guard(metric=parts[0], op=parts[1], threshold=threshold)

Petri Net Adapter

Bridge between UPDEState and the Petri net FSM.

PetriNetAdapter(
    net: PetriNet,
    initial_marking: Marking,
    place_to_regime: dict[str, str],  # maps place names to regime names
    event_bus: EventBus | None = None,
)

adapter.step(ctx) evaluates the Petri net with the given context and returns the current Regime based on which place holds the token.

petri_adapter

Adapter from guarded Petri-net markings into supervisor regime decisions.

PetriNetAdapter validates the Petri net, initial marking, place-to-regime mapping, optional event bus, and finite metric context before stepping. It fires at most one transition through the underlying net, emits a transition event when configured, and maps active places to the highest-severity regime. It does not emit control actions directly.

Classes

PetriNetAdapter

PetriNetAdapter(
    net: PetriNet,
    initial_marking: Marking,
    place_to_regime: dict[str, str],
    event_bus: EventBus | None = None,
)

Map Petri net markings to Regime values.

Each place in the net maps to a Regime via place_to_regime. When multiple places are marked, the highest-severity regime wins (CRITICAL > RECOVERY > DEGRADED > NOMINAL).

Source code in src/scpn_phase_orchestrator/supervisor/petri_adapter.py
def __init__(
    self,
    net: PetriNet,
    initial_marking: Marking,
    place_to_regime: dict[str, str],
    event_bus: EventBus | None = None,
) -> None:
    if not isinstance(net, PetriNet):
        raise PolicyError(f"net must be a PetriNet, got {net!r}")
    if not isinstance(initial_marking, Marking):
        raise PolicyError(
            f"initial_marking must be a Marking, got {initial_marking!r}"
        )
    if not isinstance(place_to_regime, Mapping):
        raise PolicyError("place_to_regime must be a mapping")
    if not place_to_regime:
        raise PolicyError("place_to_regime must not be empty")
    if event_bus is not None and not isinstance(event_bus, EventBus):
        raise PolicyError(f"event_bus must be an EventBus, got {event_bus!r}")
    self._net = net
    self._marking = initial_marking
    self._place_to_regime: dict[str, Regime] = {}
    for place, regime_str in place_to_regime.items():
        if not isinstance(place, str) or not place.strip():
            raise PolicyError(
                f"place mapping key must be non-empty string, got {place!r}"
            )
        place_key = place.strip()
        if not isinstance(regime_str, str) or not regime_str.strip():
            raise PolicyError(
                f"regime mapping value for place {place!r} must be "
                f"non-empty string, got {regime_str!r}"
            )
        if place_key not in net.place_names:
            raise PolicyError(f"unknown place {place!r} in regime mapping")
        key = regime_str.strip().upper()
        if key not in _REGIME_LOOKUP:
            raise PolicyError(f"unknown regime {regime_str!r} for place {place!r}")
        self._place_to_regime[place_key] = _REGIME_LOOKUP[key]
    self._event_bus = event_bus
    self._step = 0
Attributes
marking property
marking: Marking

Current Petri net marking (token distribution).

Returns

Marking Current Petri net marking (token distribution).

net property
net: PetriNet

The underlying Petri net structure.

Returns

PetriNet The underlying Petri net structure.

Methods:
step
step(ctx: dict[str, float]) -> Regime

Advance the Petri net one step and return the active regime.

Parameters

ctx : dict[str, float] Context metric values keyed by guard-metric name.

Returns

Regime The active regime after advancing the net one step.

Source code in src/scpn_phase_orchestrator/supervisor/petri_adapter.py
def step(self, ctx: dict[str, float]) -> Regime:
    """Advance the Petri net one step and return the active regime.

    Parameters
    ----------
    ctx : dict[str, float]
        Context metric values keyed by guard-metric name.

    Returns
    -------
    Regime
        The active regime after advancing the net one step.
    """
    ctx = _validate_context(ctx)
    self._step += 1
    guard_ctx = {
        metric: value
        for metric, value in ctx.items()
        if metric in self._net.guard_metrics
    }
    new_marking, fired = self._net.step(self._marking, guard_ctx)
    if fired is not None:
        self._marking = new_marking
        if self._event_bus is not None:
            self._event_bus.post(
                RegimeEvent(
                    kind="petri_transition",
                    step=self._step,
                    detail=fired.name,
                )
            )
    return self._active_regime()

Event Bus

Publish-subscribe system for supervisor events.

RegimeEvent (frozen dataclass)

Field Type Description
kind str "regime_transition" or "boundary_violation"
step int Step number when event occurred
detail str Human-readable description

EventBus

bus = EventBus(maxlen=200)
bus.subscribe(callback)
bus.post(RegimeEvent(kind="regime_transition", step=42, detail="nominal->degraded"))
bus.history  # list of all events
bus.count    # total events posted

Events are stored in a bounded deque (default 200). Subscribers are called synchronously on post().

events

Validated supervisor event records and an in-process bounded event bus.

RegimeEvent restricts event kinds and step/detail fields before publication, and EventBus records a bounded chronological history while notifying callable subscribers synchronously. The bus is process-local and passive: it does not spawn threads, persist logs, retry subscriber failures, or emit network traffic.

Classes

RegimeEvent dataclass

RegimeEvent(kind: str, step: int, detail: str = '')

Immutable event emitted on regime transitions or boundary breaches.

EventBus

EventBus(maxlen: int = 200)

Pub/sub bus for regime events with bounded history.

Source code in src/scpn_phase_orchestrator/supervisor/events.py
def __init__(self, maxlen: int = 200) -> None:
    maxlen = _validate_positive_int(maxlen, name="maxlen")
    self._subscribers: list[Callable[[RegimeEvent], None]] = []
    self._history: deque[RegimeEvent] = deque(maxlen=maxlen)
Attributes
history property
history: list[RegimeEvent]

Chronological list of all posted events.

Returns

list[RegimeEvent] Chronological list of all posted events.

count property
count: int

Number of events in history.

Returns

int Number of events in history.

Methods:
subscribe
subscribe(callback: object) -> None

Register a callback to receive future events.

Parameters

callback : object A callable invoked with each posted event.

Raises

ValueError If callback is not callable.

Source code in src/scpn_phase_orchestrator/supervisor/events.py
def subscribe(self, callback: object) -> None:
    """Register a callback to receive future events.

    Parameters
    ----------
    callback : object
        A callable invoked with each posted event.

    Raises
    ------
    ValueError
        If ``callback`` is not callable.
    """
    if not callable(callback):
        raise ValueError(f"callback must be callable, got {callback!r}")
    self._subscribers.append(callback)
unsubscribe
unsubscribe(callback: object) -> None

Remove a previously registered callback.

Parameters

callback : object A callable invoked with each posted event.

Source code in src/scpn_phase_orchestrator/supervisor/events.py
def unsubscribe(self, callback: object) -> None:
    """Remove a previously registered callback.

    Parameters
    ----------
    callback : object
        A callable invoked with each posted event.
    """
    self._subscribers = [s for s in self._subscribers if s != callback]
post
post(event: RegimeEvent) -> None

Record event in history and notify all subscribers.

Parameters

event : RegimeEvent The regime event to record and broadcast.

Raises

ValueError If event is not a RegimeEvent.

Source code in src/scpn_phase_orchestrator/supervisor/events.py
def post(self, event: RegimeEvent) -> None:
    """Record *event* in history and notify all subscribers.

    Parameters
    ----------
    event : RegimeEvent
        The regime event to record and broadcast.

    Raises
    ------
    ValueError
        If ``event`` is not a ``RegimeEvent``.
    """
    if not isinstance(event, RegimeEvent):
        raise ValueError(f"event must be a RegimeEvent, got {event!r}")
    self._history.append(event)
    for cb in tuple(self._subscribers):
        cb(event)
clear
clear() -> None

Discard all recorded events.

Source code in src/scpn_phase_orchestrator/supervisor/events.py
def clear(self) -> None:
    """Discard all recorded events."""
    self._history.clear()

Model-Predictive Controller (MPC)

Anticipatory control using Ott-Antonsen mean-field reduction.

Prediction (dataclass)

Field Type Description
R_predicted list[float] Predicted R trajectory (horizon steps)
will_degrade bool R predicted to cross DEGRADED threshold
will_critical bool R predicted to cross CRITICAL threshold
steps_to_degradation int Steps until predicted degradation

PredictiveSupervisor

PredictiveSupervisor(
    n_oscillators: int,
    dt: float,
    horizon: int = 10,              # prediction steps ahead
    divergence_threshold: float = 0.3,  # OA model trust threshold
)

Methods:

  • predict(phases, omegas, knm, alpha) → Prediction — runs OA forward model for horizon steps, returns trajectory
  • decide(phases, omegas, knm, alpha, upde_state, boundary_state) → list[ControlAction] — predicts then acts if degradation imminent

phases, omegas, knm, and alpha are finite real-valued arrays. Boolean aliases and complex/object-complex payloads are rejected before OA prediction so the forward model cannot silently reinterpret non-physical inputs as real oscillator states, frequencies, coupling, or phase-lag matrices.

Safety fallback

When |R_predicted - R_measured| > divergence_threshold, the MPC discards its prediction and falls back to reactive control. This prevents acting on a forward model that has lost accuracy.

Computational advantage

The OA reduction is O(1) per step (single complex ODE) versus O(N) for the full Kuramoto model. For N=1000 oscillators with horizon=10, MPC prediction costs ~10 ODE steps versus 10000 Euler steps.

predictive

Predictive and free-energy supervisor diagnostics for bounded action proposals.

The module provides Ott-Antonsen horizon prediction, variational free-energy assessment, and hierarchy-level FEP assessments over validated phase/frequency state. Predictive supervisors emit conservative ControlAction proposals for degradation, critical forecasts, hard boundaries, or high surprise. They do not apply actuation or mutate caller-owned phase/coupling arrays.

Classes

Prediction dataclass

Prediction(
    R_predicted: list[float],
    will_degrade: bool,
    will_critical: bool,
    steps_to_degradation: int,
)

Forward model output: predicted R trajectory and degradation flags.

FEPPredictionAssessment dataclass

FEPPredictionAssessment(
    free_energy: float,
    complexity: float,
    mean_abs_error: float,
    precision_mean: float,
    precision_spread: float,
    observed_R: float,
    observed_psi: float,
    predicted_R: float,
    target_R: float,
    surprise: float,
)

One-step variational free-energy assessment for supervisor control.

Attributes
above_target property
above_target: bool

Return True when observed coherence exceeds the target.

Returns

bool Return True when observed coherence exceeds the target.

Methods:
to_audit_record
to_audit_record() -> dict[str, float]

Return a serialisable audit payload.

Returns

dict[str, float] Return a serialisable audit payload.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def to_audit_record(self) -> dict[str, float]:
    """Return a serialisable audit payload.

    Returns
    -------
    dict[str, float]
        Return a serialisable audit payload.
    """
    return {
        "free_energy": self.free_energy,
        "complexity": self.complexity,
        "mean_abs_error": self.mean_abs_error,
        "precision_mean": self.precision_mean,
        "precision_spread": self.precision_spread,
        "observed_R": self.observed_R,
        "observed_psi": self.observed_psi,
        "predicted_R": self.predicted_R,
        "target_R": self.target_R,
        "surprise": self.surprise,
    }

FEPHierarchyChildAssessment dataclass

FEPHierarchyChildAssessment(
    name: str,
    assessment: FEPPredictionAssessment,
    actions: tuple[ControlAction, ...],
)

Assessment for one child node in a hierarchical FEP supervisor.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe child hierarchy audit record.

Returns

dict[str, object] Return a JSON-safe child hierarchy audit record.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe child hierarchy audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe child hierarchy audit record.
    """
    return {
        "name": self.name,
        "assessment": self.assessment.to_audit_record(),
        "actions": [_action_record(action) for action in self.actions],
    }

FEPHierarchyAssessment dataclass

FEPHierarchyAssessment(
    hierarchy: str,
    children: tuple[FEPHierarchyChildAssessment, ...],
    parent_assessment: FEPPredictionAssessment,
    parent_actions: tuple[ControlAction, ...],
    child_R_values: tuple[float, ...],
    parent_phase_encoding: tuple[float, ...],
)

Audit-ready child-to-parent FEP hierarchy assessment.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe hierarchy assessment payload.

Returns

dict[str, object] Return a JSON-safe hierarchy assessment payload.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe hierarchy assessment payload.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe hierarchy assessment payload.
    """
    return {
        "hierarchy": self.hierarchy,
        "children": [child.to_audit_record() for child in self.children],
        "parent": {
            "assessment": self.parent_assessment.to_audit_record(),
            "actions": [_action_record(action) for action in self.parent_actions],
        },
        "child_R_values": list(self.child_R_values),
        "parent_phase_encoding": list(self.parent_phase_encoding),
    }

PredictiveSupervisor

PredictiveSupervisor(
    n_oscillators: int,
    dt: float,
    horizon: int = 10,
    divergence_threshold: float = 0.3,
)

Model-predictive supervisor using Ott-Antonsen forward model.

Predicts R trajectory horizon steps ahead. Acts preemptively when predicted R crosses thresholds, instead of waiting for actual degradation. Falls back to reactive supervision if OA prediction diverges.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def __init__(
    self,
    n_oscillators: int,
    dt: float,
    horizon: int = 10,
    divergence_threshold: float = 0.3,
):
    self._n = _require_positive_int(n_oscillators, "n_oscillators")
    self._dt = _require_positive_real(dt, "dt")
    self._horizon = _require_positive_int(horizon, "horizon")
    self._divergence_threshold = non_negative_real(
        divergence_threshold,
        name="divergence_threshold",
    )
Methods:
predict
predict(
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
) -> Prediction

Predict R trajectory using OA reduction as fast forward model.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). omegas : FloatArray Natural frequencies in rad/s, shape (N,). knm : FloatArray Coupling matrix K_nm, shape (N, N). alpha : FloatArray Phase-lag matrix in radians, shape (N, N), or None for no lag.

Returns

Prediction The predicted R trajectory.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def predict(
    self,
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
) -> Prediction:
    """Predict R trajectory using OA reduction as fast forward model.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    omegas : FloatArray
        Natural frequencies in rad/s, shape ``(N,)``.
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    alpha : FloatArray
        Phase-lag matrix in radians, shape ``(N, N)``, or ``None`` for no lag.

    Returns
    -------
    Prediction
        The predicted ``R`` trajectory.
    """
    phases, omegas, knm, alpha = _validate_predictive_inputs(
        phases,
        omegas,
        knm,
        alpha,
        self._n,
    )
    R_current, psi = compute_order_parameter(phases)

    # Fit Lorentzian to omegas for OA
    omega_0 = float(np.median(omegas))
    q75, q25 = np.percentile(omegas, [75, 25])
    delta = max((q75 - q25) / 2.0, 0.01)
    K_eff = float(np.mean(knm[knm > 0])) if np.any(knm > 0) else 0.0

    oa = OttAntonsenReduction(omega_0, delta, K_eff, dt=self._dt)
    z0 = complex(R_current * np.cos(psi), R_current * np.sin(psi))

    trajectory = [R_current]
    z = z0
    for _ in range(self._horizon):
        z = oa.step(z)
        trajectory.append(abs(z))

    # Check for divergence (OA prediction unreliable)
    if abs(trajectory[-1] - R_current) > self._divergence_threshold:
        trajectory = [R_current] * (self._horizon + 1)

    will_degrade = any(r < _R_DEGRADED for r in trajectory)
    will_critical = any(r < _R_CRITICAL for r in trajectory)
    steps_to_deg = self._horizon
    for i, r in enumerate(trajectory):
        if r < _R_DEGRADED:
            steps_to_deg = i
            break

    return Prediction(
        R_predicted=trajectory,
        will_degrade=will_degrade,
        will_critical=will_critical,
        steps_to_degradation=steps_to_deg,
    )
decide
decide(
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> list[ControlAction]

Predictive control: act before degradation, not after.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). omegas : FloatArray Natural frequencies in rad/s, shape (N,). knm : FloatArray Coupling matrix K_nm, shape (N, N). alpha : FloatArray Phase-lag matrix in radians, shape (N, N), or None for no lag. upde_state : UPDEState The current UPDE state. boundary_state : BoundaryState The current boundary-observer state.

Returns

list[ControlAction] The predictive control actions for the current state.

Raises

ValueError If the state inputs are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def decide(
    self,
    phases: FloatArray,
    omegas: FloatArray,
    knm: FloatArray,
    alpha: FloatArray,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> list[ControlAction]:
    """Predictive control: act before degradation, not after.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    omegas : FloatArray
        Natural frequencies in rad/s, shape ``(N,)``.
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    alpha : FloatArray
        Phase-lag matrix in radians, shape ``(N, N)``, or ``None`` for no lag.
    upde_state : UPDEState
        The current UPDE state.
    boundary_state : BoundaryState
        The current boundary-observer state.

    Returns
    -------
    list[ControlAction]
        The predictive control actions for the current state.

    Raises
    ------
    ValueError
        If the state inputs are invalid.
    """
    if not isinstance(upde_state, UPDEState):
        raise ValueError(f"upde_state must be a UPDEState, got {upde_state!r}")
    if not isinstance(boundary_state, BoundaryState):
        raise ValueError(
            f"boundary_state must be a BoundaryState, got {boundary_state!r}"
        )
    if boundary_state.hard_violations:
        return [
            ControlAction(
                knob="zeta",
                scope="global",
                value=0.1,
                ttl_s=5.0,
                justification="hard boundary violation",
            )
        ]

    pred = self.predict(phases, omegas, knm, alpha)

    if pred.will_critical:
        return [
            ControlAction(
                knob="K",
                scope="global",
                value=_K_BOOST * 2,
                ttl_s=10.0,
                justification=(
                    f"MPC: R predicted to hit CRITICAL "
                    f"in {pred.steps_to_degradation} steps"
                ),
            )
        ]

    if pred.will_degrade and pred.steps_to_degradation < self._horizon // 2:
        return [
            ControlAction(
                knob="K",
                scope="global",
                value=_K_BOOST,
                ttl_s=10.0,
                justification=(
                    f"MPC: R predicted to degrade "
                    f"in {pred.steps_to_degradation} steps"
                ),
            )
        ]

    return []

FEPPredictiveSupervisor

FEPPredictiveSupervisor(
    n_oscillators: int,
    dt: float,
    target_R: float = 0.8,
    free_energy_threshold: float = 1.0,
    error_threshold: float = 0.25,
    drive_gain: float = 0.1,
    learning_rate: float = 0.01,
    prior_precision: float = 1.0,
)

Free-energy predictive supervisor built on VariationalPredictor.

The class turns the existing FEP-Kuramoto variational predictor into a bounded supervisor mode. It does not claim a complete biological FEP model; it exposes an auditable one-step free-energy signal and maps high surprise into conservative zeta / Psi control actions.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def __init__(
    self,
    n_oscillators: int,
    dt: float,
    target_R: float = 0.8,
    free_energy_threshold: float = 1.0,
    error_threshold: float = 0.25,
    drive_gain: float = 0.1,
    learning_rate: float = 0.01,
    prior_precision: float = 1.0,
) -> None:
    n_oscillators = _require_positive_int(n_oscillators, "n_oscillators")
    dt = _require_positive_real(dt, "dt")
    _require_unit_interval(target_R, "target_R")
    non_negative_real(free_energy_threshold, name="free_energy_threshold")
    non_negative_real(error_threshold, name="error_threshold")
    non_negative_real(drive_gain, name="drive_gain")
    non_negative_real(learning_rate, name="learning_rate")
    non_negative_real(prior_precision, name="prior_precision")

    self._n = n_oscillators
    self._dt = dt
    self._target_R = target_R
    self._free_energy_threshold = free_energy_threshold
    self._error_threshold = error_threshold
    self._drive_gain = drive_gain
    self._predictor = VariationalPredictor(
        n_oscillators,
        prior_precision=prior_precision,
        learning_rate=learning_rate,
    )
    self._last_assessment: FEPPredictionAssessment | None = None
Attributes
target_R property
target_R: float

Target order parameter used by the free-energy controller.

Returns

float Target order parameter used by the free-energy controller.

last_assessment property
last_assessment: FEPPredictionAssessment | None

Most recent free-energy assessment, if assess has run.

Returns

FEPPredictionAssessment | None Most recent free-energy assessment, if assess has run.

Methods:
assess
assess(
    phases: FloatArray, omegas: FloatArray
) -> FEPPredictionAssessment

Update the variational predictor and return audit-ready metrics.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). omegas : FloatArray Natural frequencies in rad/s, shape (N,).

Returns

FEPPredictionAssessment The free-energy prediction assessment.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def assess(self, phases: FloatArray, omegas: FloatArray) -> FEPPredictionAssessment:
    """Update the variational predictor and return audit-ready metrics.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    omegas : FloatArray
        Natural frequencies in rad/s, shape ``(N,)``.

    Returns
    -------
    FEPPredictionAssessment
        The free-energy prediction assessment.
    """
    phases_arr, omegas_arr = _validate_phase_inputs(phases, omegas, self._n)
    variational = self._predictor.update(phases_arr, omegas_arr, self._dt)
    observed_R, observed_psi = compute_order_parameter(phases_arr)
    predicted_R, _ = compute_order_parameter(variational.predicted_phases)
    mean_abs_error = float(np.mean(np.abs(variational.error)))
    precision_mean = float(np.mean(variational.precision))
    precision_spread = float(
        np.max(variational.precision) - np.min(variational.precision)
    )
    surprise = float(abs(observed_R - self._target_R) + mean_abs_error)
    assessment = FEPPredictionAssessment(
        free_energy=float(variational.free_energy),
        complexity=float(variational.complexity),
        mean_abs_error=mean_abs_error,
        precision_mean=precision_mean,
        precision_spread=precision_spread,
        observed_R=observed_R,
        observed_psi=observed_psi,
        predicted_R=predicted_R,
        target_R=self._target_R,
        surprise=surprise,
    )
    self._last_assessment = assessment
    return assessment
decide
decide(
    phases: FloatArray,
    omegas: FloatArray,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> list[ControlAction]

Return FEP-MPC control actions for the current observation.

Parameters

phases : FloatArray Oscillator phases in radians, shape (N,). omegas : FloatArray Natural frequencies in rad/s, shape (N,). upde_state : UPDEState The current UPDE state. boundary_state : BoundaryState The current boundary-observer state.

Returns

list[ControlAction] The FEP-MPC control actions for the current observation.

Raises

ValueError If the state inputs are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def decide(
    self,
    phases: FloatArray,
    omegas: FloatArray,
    upde_state: UPDEState,
    boundary_state: BoundaryState,
) -> list[ControlAction]:
    """Return FEP-MPC control actions for the current observation.

    Parameters
    ----------
    phases : FloatArray
        Oscillator phases in radians, shape ``(N,)``.
    omegas : FloatArray
        Natural frequencies in rad/s, shape ``(N,)``.
    upde_state : UPDEState
        The current UPDE state.
    boundary_state : BoundaryState
        The current boundary-observer state.

    Returns
    -------
    list[ControlAction]
        The FEP-MPC control actions for the current observation.

    Raises
    ------
    ValueError
        If the state inputs are invalid.
    """
    if not isinstance(upde_state, UPDEState):
        raise ValueError(f"upde_state must be a UPDEState, got {upde_state!r}")
    if not isinstance(boundary_state, BoundaryState):
        raise ValueError(
            f"boundary_state must be a BoundaryState, got {boundary_state!r}"
        )
    if boundary_state.hard_violations:
        return [
            ControlAction(
                knob="zeta",
                scope="global",
                value=self._drive_gain,
                ttl_s=5.0,
                justification="FEP-MPC: hard boundary violation",
            )
        ]

    assessment = self.assess(phases, omegas)
    if not self._should_act(assessment, upde_state):
        return []

    psi_target = assessment.observed_psi
    if assessment.above_target:
        psi_target = (psi_target + np.pi) % TWO_PI

    return [
        ControlAction(
            knob="zeta",
            scope="global",
            value=self._drive_gain,
            ttl_s=5.0,
            justification=(
                "FEP-MPC: free energy "
                f"{assessment.free_energy:.4g}, surprise "
                f"{assessment.surprise:.4g}"
            ),
        ),
        ControlAction(
            knob="Psi",
            scope="global",
            value=float(psi_target),
            ttl_s=5.0,
            justification="FEP-MPC: precision-weighted phase target",
        ),
    ]
reset
reset() -> None

Reset the underlying variational predictor and cached assessment.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def reset(self) -> None:
    """Reset the underlying variational predictor and cached assessment."""
    self._predictor.reset()
    self._last_assessment = None

Functions:

assess_fep_hierarchy

assess_fep_hierarchy(
    children: Mapping[str, tuple[FloatArray, FloatArray]],
    *,
    dt: float,
    child_target_R: float = 0.8,
    parent_target_R: float = 0.8,
    parent_dt: float | None = None,
    free_energy_threshold: float = 0.0,
    child_drive_gain: float = 0.08,
    parent_drive_gain: float = 0.05,
    hierarchy: str = "child_regions_to_parent_fep_supervisor",
) -> FEPHierarchyAssessment

Assess child FEP supervisors and a parent over reduced child coherence.

Each child receives its own FEPPredictiveSupervisor. The parent encodes child coherence as phases via arccos(2R - 1) so the same FEP machinery can reason over cross-child coherence without accessing raw child signals.

Parameters

children : Mapping[str, tuple[FloatArray, FloatArray]] Child supervisor summaries. dt : float Integration step size. child_target_R : float Target order parameter for each child. parent_target_R : float Target order parameter for the parent. parent_dt : float | None Parent integration step size, or None. free_energy_threshold : float Free-energy threshold above which control acts. child_drive_gain : float Drive gain applied at the child level. parent_drive_gain : float Drive gain applied at the parent level. hierarchy : str Hierarchy label.

Returns

FEPHierarchyAssessment The hierarchical free-energy assessment.

Source code in src/scpn_phase_orchestrator/supervisor/predictive.py
def assess_fep_hierarchy(
    children: Mapping[str, tuple[FloatArray, FloatArray]],
    *,
    dt: float,
    child_target_R: float = 0.8,
    parent_target_R: float = 0.8,
    parent_dt: float | None = None,
    free_energy_threshold: float = 0.0,
    child_drive_gain: float = 0.08,
    parent_drive_gain: float = 0.05,
    hierarchy: str = "child_regions_to_parent_fep_supervisor",
) -> FEPHierarchyAssessment:
    """Assess child FEP supervisors and a parent over reduced child coherence.

    Each child receives its own ``FEPPredictiveSupervisor``. The parent encodes
    child coherence as phases via ``arccos(2R - 1)`` so the same FEP machinery
    can reason over cross-child coherence without accessing raw child signals.

    Parameters
    ----------
    children : Mapping[str, tuple[FloatArray, FloatArray]]
        Child supervisor summaries.
    dt : float
        Integration step size.
    child_target_R : float
        Target order parameter for each child.
    parent_target_R : float
        Target order parameter for the parent.
    parent_dt : float | None
        Parent integration step size, or ``None``.
    free_energy_threshold : float
        Free-energy threshold above which control acts.
    child_drive_gain : float
        Drive gain applied at the child level.
    parent_drive_gain : float
        Drive gain applied at the parent level.
    hierarchy : str
        Hierarchy label.

    Returns
    -------
    FEPHierarchyAssessment
        The hierarchical free-energy assessment.
    """
    _validate_hierarchy_inputs(
        children=children,
        dt=dt,
        parent_dt=parent_dt,
        child_target_R=child_target_R,
        parent_target_R=parent_target_R,
        free_energy_threshold=free_energy_threshold,
        child_drive_gain=child_drive_gain,
        parent_drive_gain=parent_drive_gain,
    )
    child_records: list[FEPHierarchyChildAssessment] = []
    child_rs: list[float] = []
    for name, (phases, omegas) in children.items():
        phases_arr, omegas_arr = _validate_child_observation(name, phases, omegas)
        supervisor = FEPPredictiveSupervisor(
            n_oscillators=phases_arr.size,
            dt=dt,
            target_R=child_target_R,
            free_energy_threshold=free_energy_threshold,
            drive_gain=child_drive_gain,
        )
        assessment = supervisor.assess(phases_arr, omegas_arr)
        actions = tuple(
            supervisor.decide(
                phases_arr,
                omegas_arr,
                _state_from_r(assessment.observed_R),
                BoundaryState(),
            )
        )
        child_records.append(
            FEPHierarchyChildAssessment(
                name=name,
                assessment=assessment,
                actions=actions,
            )
        )
        child_rs.append(assessment.observed_R)

    child_r_arr = np.asarray(child_rs, dtype=np.float64)
    parent_phases = _coherence_to_parent_phases(child_r_arr)
    parent_omegas = np.full(parent_phases.shape, 1.0, dtype=np.float64)
    parent = FEPPredictiveSupervisor(
        n_oscillators=parent_phases.size,
        dt=dt if parent_dt is None else parent_dt,
        target_R=parent_target_R,
        free_energy_threshold=free_energy_threshold,
        drive_gain=parent_drive_gain,
    )
    parent_assessment = parent.assess(parent_phases, parent_omegas)
    parent_actions = tuple(
        parent.decide(
            parent_phases,
            parent_omegas,
            _state_from_r(parent_assessment.observed_R),
            BoundaryState(),
        )
    )
    return FEPHierarchyAssessment(
        hierarchy=hierarchy,
        children=tuple(child_records),
        parent_assessment=parent_assessment,
        parent_actions=parent_actions,
        child_R_values=tuple(float(value) for value in child_r_arr),
        parent_phase_encoding=tuple(float(value) for value in parent_phases),
    )

FEP Predictive Supervisor

FEPPredictiveSupervisor is the first Python supervisor mode that uses the existing VariationalPredictor as an auditable free-energy signal. It observes the current phase vector, updates the variational predictor, and emits bounded zeta / Psi actions only when free energy, prediction error, or stability proxy thresholds indicate a pre-emptive correction is needed.

from scpn_phase_orchestrator.supervisor import (
    FEPPredictiveSupervisor,
    assess_fep_hierarchy,
)

fep = FEPPredictiveSupervisor(
    n_oscillators=len(phases),
    dt=0.01,
    target_R=0.8,
    free_energy_threshold=1.0,
)
assessment = fep.assess(phases, omegas)
actions = fep.decide(phases, omegas, upde_state, boundary_state)
audit_payload = assessment.to_audit_record()

FEPPredictionAssessment records free energy, complexity, mean absolute prediction error, precision statistics, observed and predicted order parameters, target R, and a scalar surprise proxy. This keeps the FEP path reviewable in the same audit trail as policy, causal, STL, and topology decisions.

This slice is intentionally conservative: it is a FEP-Kuramoto correspondence controller over the existing variational predictor, not a claim of a complete biological active-inference agent.

assess_fep_hierarchy() is the reusable hierarchy primitive. It runs one child FEPPredictiveSupervisor per named child observation, reduces each child's observed coherence into a parent phase vector, then runs a parent FEPPredictiveSupervisor over the reduced child state. The returned FEPHierarchyAssessment records child assessments, child actions, parent assessment, parent actions, child R values, and parent phase encoding. Child phase and frequency observations use the same finite real-valued boundary contract as the single-supervisor path.

hierarchy = assess_fep_hierarchy(
    {
        "generation_area": (generation_phases, generation_omegas),
        "demand_area": (demand_phases, demand_omegas),
    },
    dt=0.01,
    parent_dt=0.1,
)
audit_hierarchy = hierarchy.to_audit_record()

Domainpack hierarchy proofs:

  • domainpacks/power_grid/fep_hierarchy_demo.py runs generation and demand/renewable child regions into a parent grid supervisor.
  • domainpacks/cardiac_rhythm/fep_hierarchy_demo.py runs pacemaker/atrial and ventricular/recovery child axes into a parent cardiac supervisor.

Performance summary

Operation Budget Notes
RegimeManager.evaluate() < 10 μs Pure Python comparison
SupervisorPolicy.decide() < 50 μs Rule evaluation + action construction
PetriNet.enabled() < 10 μs Guard evaluation
PredictiveSupervisor.predict() < 1 ms OA mean-field (10 complex ODE steps)
EventBus.post() < 5 μs Synchronous dispatch

Active Inference Agent

The ActiveInferenceAgent provides a predictive candidate-generation framework based on a Variational Free Energy objective. It is an optional Rust-backed research surface; its outputs still require the normal policy, projection, audit, and operator-review boundaries.

Mathematical Model

The agent maintains a low-dimensional internal state \(x\) and minimises a Variational Free Energy objective \(F\) between its prediction \(\hat{R}\) and the observed coherence \(R_{\mathrm{obs}}\):

\[ F \approx \int q(x) \ln \frac{q(x)}{p(R_{\mathrm{obs}}, x)}\,dx \]

The agent proposes a forcing strength \(\zeta\) and reference phase \(\Psi\) for a configured target coherence \(R_{\mathrm{target}}\). “Optimal” is relative to the implemented local objective and does not establish domain-level optimality or safe actuation.

Features

  • Candidate suppression: can propose anti-phase driving (\(\Psi = \psi + \pi\)) against a configured coherence objective.
  • Rust implementation: available through the optional spo_kernel FFI; no portable latency or hard-real-time guarantee is claimed.
  • Prediction-error adaptation: updates its internal state in response to observed divergence; robustness to domain drift requires separate evidence.

Rust-only module

ActiveInferenceAgent is implemented in spo-kernel (Rust crate spo-supervisor::active_inference). Python access via spo_kernel.PyActiveInferenceAgent when the FFI is installed.

Evolutionary Review Surfaces

Offline-only evolutionary search, grammar, policy DSL, topology mutation, and example builders used for non-actuating supervisor review workflows.

evolutionary_examples

Deterministic examples for offline evolutionary supervisor policy search.

Functions:

build_evolutionary_supervisor_search_examples

build_evolutionary_supervisor_search_examples() -> (
    tuple[dict[str, object], ...]
)

Return deterministic offline-search example inputs for reference gates.

Returns

tuple[dict[str, object], ...] Return deterministic offline-search example inputs for reference gates.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_examples.py
def build_evolutionary_supervisor_search_examples() -> tuple[dict[str, object], ...]:
    """Return deterministic offline-search example inputs for reference gates.

    Returns
    -------
    tuple[dict[str, object], ...]
        Return deterministic offline-search example inputs for reference gates.
    """
    records = _build_examples()
    for record in records:
        _validate_evolutionary_supervisor_search_record(record)
    return records

build_evolutionary_supervisor_search_examples_from_worker_a_api

build_evolutionary_supervisor_search_examples_from_worker_a_api() -> (
    tuple[dict[str, object], ...]
)

Return examples enriched with core offline-search report counts.

Returns

tuple[dict[str, object], ...] Return examples enriched with core offline-search report counts.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_examples.py
def build_evolutionary_supervisor_search_examples_from_worker_a_api() -> tuple[
    dict[str, object], ...
]:
    """Return examples enriched with core offline-search report counts.

    Returns
    -------
    tuple[dict[str, object], ...]
        Return examples enriched with core offline-search report counts.
    """
    enriched: list[dict[str, object]] = []
    for record in build_evolutionary_supervisor_search_examples():
        report = run_offline_evolutionary_supervisor_search(
            _as_mapping(record["parent_policy"], label="parent_policy"),
            _as_mapping_sequence(record["audit_replays"], label="audit_replays"),
            stl_spec=str(record["stl_spec"]),
            trace=_as_trace(record["trace"]),
            generation_count=_as_int(
                record["generation_count"],
                label="generation_count",
            ),
            population_size=_as_int(record["population_size"], label="population_size"),
            mutation_step=_as_float(record["mutation_step"], label="mutation_step"),
            minimum_replay_reward=_as_float(
                record["minimum_replay_reward"], label="minimum_replay_reward"
            ),
            minimum_safety_margin=_as_float(
                record["minimum_safety_margin"], label="minimum_safety_margin"
            ),
        )
        merged = dict(record)
        merged.update(
            {
                "candidate_count": report.candidate_count,
                "accepted_candidate_count": report.accepted_count,
                "rejected_candidate_count": report.rejected_count,
                "report_hash": report.report_hash,
            }
        )
        enriched.append(merged)
    return tuple(enriched)

evolutionary_petri_grammar

Review-only offline evolutionary mutation grammar for Petri-net topologies.

This module produces deterministic mutation candidates and plans from a simple, normalised net descriptor. It performs no execution, no actuation, and never commits graph changes itself.

Classes

EvolutionaryPetriMutationConfig dataclass

EvolutionaryPetriMutationConfig(
    generation_count: int = 2,
    candidates_per_generation: int = 6,
    mutation_step: float = 0.1,
    max_arc_weight: int = 4,
    max_token_bound: int = 128,
)

Mutation generation configuration for the offline Petri grammar.

EvolutionaryPetriMutationCandidate dataclass

EvolutionaryPetriMutationCandidate(
    candidate_id: str,
    generation: int,
    mutation_type: MutationType,
    mutation_target: str,
    mutation_kind: str,
    blocked_reasons: tuple[str, ...],
    before: dict[str, object],
    after: dict[str, object],
    mutation_delta: float,
    candidate_hash: str,
    operator_review_required: bool = True,
    execution_disabled: bool = True,
    live_merge_permitted: bool = False,
    hot_patch_permitted: bool = False,
    actuation_permitted: bool = False,
)

One offline-only Petri-net mutation candidate.

Attributes
accepted property
accepted: bool

Return whether this candidate is accepted for review.

Returns

bool Return whether this candidate is accepted for review.

status property
status: str

Return the review status label for this candidate.

Returns

str Return the review status label for this candidate.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_petri_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "candidate_id": self.candidate_id,
        "generation": self.generation,
        "mutation_type": self.mutation_type,
        "mutation_target": self.mutation_target,
        "mutation_kind": self.mutation_kind,
        "blocked_reasons": list(self.blocked_reasons),
        "before": dict(self.before),
        "after": dict(self.after),
        "mutation_delta": self.mutation_delta,
        "status": self.status,
        "candidate_hash": self.candidate_hash,
        "operator_review_required": self.operator_review_required,
        "execution_disabled": self.execution_disabled,
        "live_merge_permitted": self.live_merge_permitted,
        "hot_patch_permitted": self.hot_patch_permitted,
        "actuation_permitted": self.actuation_permitted,
    }

EvolutionaryPetriMutationPlan dataclass

EvolutionaryPetriMutationPlan(
    schema_name: str,
    schema_version: str,
    config: EvolutionaryPetriMutationConfig,
    source_net_hash: str,
    candidate_count: int,
    accepted_count: int,
    rejected_count: int,
    candidates: tuple[
        EvolutionaryPetriMutationCandidate, ...
    ],
    best_candidate_id: str | None,
    source_net: dict[str, object],
    operator_review_required: bool,
    execution_disabled: bool,
    live_merge_permitted: bool,
    hot_patch_permitted: bool,
    actuation_permitted: bool,
    non_actuating: bool,
    plan_hash: str,
)

Deterministic, offline review plan for Petri-net grammar search.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_petri_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "generation_count": self.config.generation_count,
        "candidates_per_generation": self.config.candidates_per_generation,
        "mutation_step": self.config.mutation_step,
        "max_arc_weight": self.config.max_arc_weight,
        "max_token_bound": self.config.max_token_bound,
        "source_net_hash": self.source_net_hash,
        "source_net": self.source_net,
        "candidate_count": self.candidate_count,
        "accepted_count": self.accepted_count,
        "rejected_count": self.rejected_count,
        "best_candidate_id": self.best_candidate_id,
        "candidates": [
            candidate.to_audit_record() for candidate in self.candidates
        ],
        "operator_review_required": self.operator_review_required,
        "execution_disabled": self.execution_disabled,
        "live_merge_permitted": self.live_merge_permitted,
        "hot_patch_permitted": self.hot_patch_permitted,
        "actuation_permitted": self.actuation_permitted,
        "non_actuating": self.non_actuating,
        "claim_boundary": "offline_petri_mutation_review_only",
        "plan_hash": self.plan_hash,
    }

Functions:

run_offline_evolutionary_petri_mutation_grammar

run_offline_evolutionary_petri_mutation_grammar(
    net_like: Mapping[str, object] | Sequence[object],
    *,
    generation_count: int = 2,
    candidates_per_generation: int = 6,
    mutation_step: float = 0.1,
    max_arc_weight: int = 4,
    max_token_bound: int = 128,
) -> EvolutionaryPetriMutationPlan

Build a deterministic review-only mutation plan from a net-like payload.

Parameters

net_like : Mapping[str, object] | Sequence[object] A net-like payload describing places, transitions, and arcs. generation_count : int Number of search generations. candidates_per_generation : int Number of candidates evaluated per generation. mutation_step : float Mutation step size applied per generation. max_arc_weight : int Maximum arc weight allowed in a mutated net. max_token_bound : int Maximum token count allowed per place.

Returns

EvolutionaryPetriMutationPlan The review-only Petri mutation plan.

Raises

ValueError If the net-like payload or bounds are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_petri_grammar.py
def run_offline_evolutionary_petri_mutation_grammar(
    net_like: Mapping[str, object] | Sequence[object],
    *,
    generation_count: int = 2,
    candidates_per_generation: int = 6,
    mutation_step: float = 0.1,
    max_arc_weight: int = 4,
    max_token_bound: int = 128,
) -> EvolutionaryPetriMutationPlan:
    """Build a deterministic review-only mutation plan from a net-like payload.

    Parameters
    ----------
    net_like : Mapping[str, object] | Sequence[object]
        A net-like payload describing places, transitions, and arcs.
    generation_count : int
        Number of search generations.
    candidates_per_generation : int
        Number of candidates evaluated per generation.
    mutation_step : float
        Mutation step size applied per generation.
    max_arc_weight : int
        Maximum arc weight allowed in a mutated net.
    max_token_bound : int
        Maximum token count allowed per place.

    Returns
    -------
    EvolutionaryPetriMutationPlan
        The review-only Petri mutation plan.

    Raises
    ------
    ValueError
        If the net-like payload or bounds are invalid.
    """
    config = EvolutionaryPetriMutationConfig(
        generation_count=generation_count,
        candidates_per_generation=candidates_per_generation,
        mutation_step=mutation_step,
        max_arc_weight=max_arc_weight,
        max_token_bound=max_token_bound,
    )

    spec = _normalise_net_like(net_like)
    base_record = spec.to_record()
    source_net_hash = _build_stable_hash(base_record)

    candidates: list[EvolutionaryPetriMutationCandidate] = []
    for generation in range(config.generation_count):
        for local_index in range(config.candidates_per_generation):
            mutation_type: MutationType = _mutation_type_at_index(
                generation=generation,
                local_index=local_index,
            )
            candidate_index = (
                generation * config.candidates_per_generation + local_index
            )
            if mutation_type == "add_arc":
                candidate = _build_add_arc_candidate(
                    spec,
                    config,
                    generation,
                    local_index,
                )
            elif mutation_type == "guard_weight":
                candidate = _build_guard_weight_candidate(
                    spec,
                    config,
                    generation,
                    local_index,
                    candidate_index,
                )
            elif mutation_type == "token_bound":
                candidate = _build_token_bound_candidate(
                    spec,
                    config,
                    generation,
                    local_index,
                    candidate_index,
                )
            else:  # pragma: no cover - defensive guard for typing completeness
                raise ValueError(f"unsupported mutation type {mutation_type}")
            candidates.append(
                replace(
                    candidate,
                    candidate_id=f"g{generation + 1:03d}-c{local_index + 1:03d}",
                    candidate_hash=_build_stable_hash(candidate.to_audit_record()),
                )
            )

    accepted = [candidate for candidate in candidates if candidate.accepted]
    best_candidate_id = max(
        accepted,
        key=lambda candidate: _candidate_score(candidate),
        default=None,
    )
    best_id = best_candidate_id.candidate_id if best_candidate_id else None

    report = EvolutionaryPetriMutationPlan(
        schema_name="evolutionary_petri_mutation_grammar",
        schema_version="0.1.0",
        config=config,
        source_net_hash=source_net_hash,
        candidate_count=len(candidates),
        accepted_count=len(accepted),
        rejected_count=len(candidates) - len(accepted),
        candidates=tuple(candidates),
        best_candidate_id=best_id,
        source_net=base_record,
        operator_review_required=True,
        execution_disabled=True,
        live_merge_permitted=False,
        hot_patch_permitted=False,
        actuation_permitted=False,
        non_actuating=True,
        plan_hash="",
    )

    return EvolutionaryPetriMutationPlan(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        config=report.config,
        source_net_hash=report.source_net_hash,
        candidate_count=report.candidate_count,
        accepted_count=report.accepted_count,
        rejected_count=report.rejected_count,
        candidates=report.candidates,
        best_candidate_id=report.best_candidate_id,
        source_net=report.source_net,
        operator_review_required=report.operator_review_required,
        execution_disabled=report.execution_disabled,
        live_merge_permitted=report.live_merge_permitted,
        hot_patch_permitted=report.hot_patch_permitted,
        actuation_permitted=report.actuation_permitted,
        non_actuating=report.non_actuating,
        plan_hash=_build_stable_hash(report.to_audit_record()),
    )

evolutionary_policy_dsl

Policy DSL mutation helpers for offline evolutionary supervisor review.

Classes

PolicyCondition dataclass

PolicyCondition(
    metric: str, operator: str, threshold: float
)

Atomic predicate in the policy mutation DSL.

Methods:
to_dsl
to_dsl() -> str

Return the deterministic policy DSL representation.

Returns

str Return the deterministic policy DSL representation.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def to_dsl(self) -> str:
    """Return the deterministic policy DSL representation.

    Returns
    -------
    str
        Return the deterministic policy DSL representation.
    """
    return f"{self.metric} {self.operator} {_format_float(self.threshold)}"

PolicyAction dataclass

PolicyAction(target: str, operator: str, value: float)

Bounded action in the policy mutation DSL.

Methods:
to_dsl
to_dsl() -> str

Return the deterministic policy DSL representation.

Returns

str Return the deterministic policy DSL representation.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def to_dsl(self) -> str:
    """Return the deterministic policy DSL representation.

    Returns
    -------
    str
        Return the deterministic policy DSL representation.
    """
    return f"set {self.target} {self.operator} {_format_float(self.value)}"

PolicyRule dataclass

PolicyRule(
    name: str,
    conditions: tuple[PolicyCondition, ...],
    action: PolicyAction,
)

Policy rule composed from conditions and actions.

Methods:
to_dsl
to_dsl() -> str

Return the deterministic policy DSL representation.

Returns

str Return the deterministic policy DSL representation.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def to_dsl(self) -> str:
    """Return the deterministic policy DSL representation.

    Returns
    -------
    str
        Return the deterministic policy DSL representation.
    """
    condition_text = " and ".join(
        condition.to_dsl() for condition in self.conditions
    )
    return f"rule {self.name}: if {condition_text} then {self.action.to_dsl()}"

PolicyMutationSearchConfig dataclass

PolicyMutationSearchConfig(
    generation_count: int = 2,
    population_size: int = 6,
    mutation_step: float = 0.05,
)

Configuration for deterministic policy mutation search.

PolicyMutationPlan dataclass

PolicyMutationPlan(
    rule_name: str,
    component: str,
    component_index: int,
    operator: str,
    original_value: float,
    mutated_value: float,
    mutation_delta: float,
)

Planned mutation candidate for policy DSL review.

PolicyMutationCandidate dataclass

PolicyMutationCandidate(
    candidate_id: str,
    generation: int,
    mutation_index: int,
    source_rule_name: str,
    source_rule_text: str,
    mutated_rule_text: str,
    candidate_policy_dsl: str,
    mutation_plan: PolicyMutationPlan,
    blocked_reasons: tuple[str, ...],
    candidate_hash: str,
    operator_review_required: bool = True,
    execution_disabled: bool = True,
    live_merge_permitted: bool = False,
    hot_patch_permitted: bool = False,
    actuation_permitted: bool = False,
)

One non-actuating policy mutation candidate.

Attributes
accepted property
accepted: bool

Return whether this candidate is accepted for review.

Returns

bool Return whether this candidate is accepted for review.

Methods:
to_audit_record
to_audit_record() -> dict[str, Any]

Return a deterministic JSON-safe audit record.

Returns

dict[str, Any] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def to_audit_record(self) -> dict[str, Any]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, Any]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "candidate_id": self.candidate_id,
        "generation": self.generation,
        "mutation_index": self.mutation_index,
        "source_rule_name": self.source_rule_name,
        "source_rule_text": self.source_rule_text,
        "mutated_rule_text": self.mutated_rule_text,
        "candidate_policy_dsl": self.candidate_policy_dsl,
        "mutation_plan": {
            "rule_name": self.mutation_plan.rule_name,
            "component": self.mutation_plan.component,
            "component_index": self.mutation_plan.component_index,
            "operator": self.mutation_plan.operator,
            "original_value": self.mutation_plan.original_value,
            "mutated_value": self.mutation_plan.mutated_value,
            "mutation_delta": self.mutation_plan.mutation_delta,
        },
        "blocked_reasons": list(self.blocked_reasons),
        "candidate_hash": self.candidate_hash,
        "status": "accepted" if self.accepted else "rejected",
        "operator_review_required": self.operator_review_required,
        "execution_disabled": self.execution_disabled,
        "live_merge_permitted": self.live_merge_permitted,
        "hot_patch_permitted": self.hot_patch_permitted,
        "actuation_permitted": self.actuation_permitted,
    }

PolicyMutationSearchReport dataclass

PolicyMutationSearchReport(
    schema_name: str,
    schema_version: str,
    config: PolicyMutationSearchConfig,
    source_policy_dsl: str,
    source_policy_hash: str,
    candidate_count: int,
    accepted_count: int,
    rejected_count: int,
    candidates: tuple[PolicyMutationCandidate, ...],
    execution_disabled: bool,
    hot_patch_permitted: bool,
    live_merge_permitted: bool,
    actuation_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    report_hash: str,
)

Aggregate report for policy mutation search.

Methods:
to_audit_record
to_audit_record() -> dict[str, Any]

Return a deterministic JSON-safe audit record.

Returns

dict[str, Any] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def to_audit_record(self) -> dict[str, Any]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, Any]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "generation_count": self.config.generation_count,
        "population_size": self.config.population_size,
        "mutation_step": self.config.mutation_step,
        "source_policy_dsl": self.source_policy_dsl,
        "source_policy_hash": self.source_policy_hash,
        "candidate_count": self.candidate_count,
        "accepted_count": self.accepted_count,
        "rejected_count": self.rejected_count,
        "candidates": [
            candidate.to_audit_record() for candidate in self.candidates
        ],
        "execution_disabled": self.execution_disabled,
        "hot_patch_permitted": self.hot_patch_permitted,
        "live_merge_permitted": self.live_merge_permitted,
        "actuation_permitted": self.actuation_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "report_hash": self.report_hash,
    }

Functions:

parse_policy_dsl

parse_policy_dsl(policy_dsl: str) -> tuple[PolicyRule, ...]

Parse immutable rule objects from a compact policy DSL string.

Parameters

policy_dsl : str A compact policy-DSL source string.

Returns

tuple[PolicyRule, ...] The immutable policy rules parsed from the DSL.

Raises

ValueError If the DSL string is malformed.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def parse_policy_dsl(policy_dsl: str) -> tuple[PolicyRule, ...]:
    """Parse immutable rule objects from a compact policy DSL string.

    Parameters
    ----------
    policy_dsl : str
        A compact policy-DSL source string.

    Returns
    -------
    tuple[PolicyRule, ...]
        The immutable policy rules parsed from the DSL.

    Raises
    ------
    ValueError
        If the DSL string is malformed.
    """
    if not isinstance(policy_dsl, str) or not policy_dsl.strip():
        raise ValueError("policy_dsl must be a non-empty string")

    rules: list[PolicyRule] = []
    seen_rule_names: set[str] = set()

    for raw_line in policy_dsl.splitlines():
        content = raw_line.split("#", 1)[0].strip()
        if not content:
            continue

        match = _RULE_RE.match(content)
        if not match:
            raise ValueError(f"Malformed rule line: {raw_line}")

        name = match.group("name")
        if name in seen_rule_names:
            raise ValueError(f"Duplicate rule name: {name}")

        seen_rule_names.add(name)

        condition_text = match.group("condition").strip()
        action_text = match.group("action").strip()

        conditions = _parse_conditions(condition_text)
        action = _parse_action(action_text)
        rules.append(PolicyRule(name=name, conditions=conditions, action=action))

    if not rules:
        raise ValueError("policy_dsl must contain at least one rule")

    return tuple(rules)
run_offline_evolutionary_policy_dsl_search(
    policy_dsl: str,
    *,
    generation_count: int = 2,
    population_size: int = 6,
    mutation_step: float = 0.05,
) -> PolicyMutationSearchReport

Generate deterministic offline policy-DSl mutation candidates for review.

Parameters

policy_dsl : str A compact policy-DSL source string. generation_count : int Number of search generations. population_size : int Number of candidates per generation. mutation_step : float Mutation step size applied per generation.

Returns

PolicyMutationSearchReport The offline policy-DSL mutation search report.

Raises

ValueError If the DSL string or search parameters are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_policy_dsl.py
def run_offline_evolutionary_policy_dsl_search(
    policy_dsl: str,
    *,
    generation_count: int = 2,
    population_size: int = 6,
    mutation_step: float = 0.05,
) -> PolicyMutationSearchReport:
    """Generate deterministic offline policy-DSl mutation candidates for review.

    Parameters
    ----------
    policy_dsl : str
        A compact policy-DSL source string.
    generation_count : int
        Number of search generations.
    population_size : int
        Number of candidates per generation.
    mutation_step : float
        Mutation step size applied per generation.

    Returns
    -------
    PolicyMutationSearchReport
        The offline policy-DSL mutation search report.

    Raises
    ------
    ValueError
        If the DSL string or search parameters are invalid.
    """
    config = PolicyMutationSearchConfig(
        generation_count=generation_count,
        population_size=population_size,
        mutation_step=mutation_step,
    )
    rules = parse_policy_dsl(policy_dsl)
    axes = _build_mutation_axes(rules)
    if not axes:
        raise ValueError("policy_dsl must contain at least one mutable component")

    source_policy_dsl = "\n".join(rule.to_dsl() for rule in rules)
    source_policy_hash = _stable_hash({"source_policy_dsl": source_policy_dsl})

    candidates: list[PolicyMutationCandidate] = []
    axis_count = len(axes)
    for generation in range(config.generation_count):
        for local_index in range(config.population_size):
            cursor = (generation * config.population_size + local_index) % axis_count
            axis = axes[cursor]
            base_delta = _deterministic_delta(
                axis_index=cursor,
                generation=generation,
                local_index=local_index,
                axis_count=axis_count,
                generation_count=config.generation_count,
                mutation_step=config.mutation_step,
            )

            original_rule = rules[axis.rule_index]
            blocked_reasons: list[str] = []
            mutated_rule = _mutate_rule(
                rule=original_rule,
                axis=axis,
                delta=base_delta,
                blocked_reasons=blocked_reasons,
            )

            mutated_rules = list(rules)
            mutated_rules[axis.rule_index] = mutated_rule
            candidate_policy = "\n".join(rule.to_dsl() for rule in mutated_rules)
            mutated_value = (
                mutated_rule.conditions[axis.component_index].threshold
                if axis.component == "condition"
                else mutated_rule.action.value
            )

            plan = PolicyMutationPlan(
                rule_name=axis.rule_name,
                component=axis.component,
                component_index=axis.component_index,
                operator=axis.operator,
                original_value=axis.original_value,
                mutated_value=mutated_value,
                mutation_delta=mutated_value - axis.original_value,
            )
            candidate = PolicyMutationCandidate(
                candidate_id=f"g{generation + 1:02d}-c{local_index + 1:03d}",
                generation=generation + 1,
                mutation_index=len(candidates),
                source_rule_name=axis.rule_name,
                source_rule_text=original_rule.to_dsl(),
                mutated_rule_text=mutated_rule.to_dsl(),
                candidate_policy_dsl=candidate_policy,
                mutation_plan=plan,
                blocked_reasons=tuple(blocked_reasons),
                candidate_hash="",
            )
            candidate = replace(
                candidate,
                candidate_hash=_build_candidate_hash(candidate),
            )
            candidates.append(candidate)

    accepted = [candidate for candidate in candidates if candidate.accepted]
    rejected = [candidate for candidate in candidates if not candidate.accepted]

    report = PolicyMutationSearchReport(
        schema_name="policy_dsl_evolution",
        schema_version="0.1.0",
        config=config,
        source_policy_dsl=source_policy_dsl,
        source_policy_hash=source_policy_hash,
        candidate_count=len(candidates),
        accepted_count=len(accepted),
        rejected_count=len(rejected),
        candidates=tuple(candidates),
        execution_disabled=True,
        hot_patch_permitted=False,
        live_merge_permitted=False,
        actuation_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        report_hash="",
    )
    return replace(report, report_hash=_build_report_hash(report))

Deterministic offline evolutionary supervisor policy search.

Classes

EvolutionarySearchConfig dataclass

EvolutionarySearchConfig(
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    minimum_replay_reward: float = 0.0,
    minimum_safety_margin: float = 0.0,
)

Configuration for deterministic offline candidate evolution.

EvolutionaryCandidate dataclass

EvolutionaryCandidate(
    candidate_id: str,
    generation: int,
    knob: str,
    parent_value: float,
    candidate_value: float,
    mutation_delta: float,
    genome: tuple[tuple[str, float], ...],
    replay_fitness: float,
    stl_robustness: float,
    stl_satisfied: bool,
    replay_violation_count: int,
    blocked_reasons: tuple[str, ...],
    candidate_hash: str,
    review_required: bool = True,
    live_merge_permitted: bool = False,
    hot_patch_permitted: bool = False,
    actuation_permitted: bool = False,
)

One offline candidate snapshot from a deterministic mutation step.

Attributes
accepted property
accepted: bool

Whether the candidate passed all guard and replay gates.

Returns

bool Whether the candidate passed all guard and replay gates.

status property
status: str

Return an export-friendly status string.

Returns

str Return an export-friendly status string.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe candidate evidence for audit transport.

Returns

dict[str, object] Return JSON-safe candidate evidence for audit transport.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_search.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe candidate evidence for audit transport.

    Returns
    -------
    dict[str, object]
        Return JSON-safe candidate evidence for audit transport.
    """
    return {
        "candidate_id": self.candidate_id,
        "generation": self.generation,
        "knob": self.knob,
        "parent_value": self.parent_value,
        "candidate_value": self.candidate_value,
        "mutation_delta": self.mutation_delta,
        "genome": [[key, value] for key, value in self.genome],
        "replay_fitness": self.replay_fitness,
        "stl_robustness": self.stl_robustness,
        "stl_satisfied": self.stl_satisfied,
        "replay_violation_count": self.replay_violation_count,
        "blocked_reasons": list(self.blocked_reasons),
        "status": self.status,
        "review_required": self.review_required,
        "live_merge_permitted": self.live_merge_permitted,
        "hot_patch_permitted": self.hot_patch_permitted,
        "actuation_permitted": self.actuation_permitted,
        "candidate_hash": self.candidate_hash,
    }

EvolutionarySearchReport dataclass

EvolutionarySearchReport(
    schema_name: str,
    schema_version: str,
    config: EvolutionarySearchConfig,
    parent_policy_hash: str,
    replay_summary: _ReplaySummary,
    stl_spec: str,
    stl_monitoring: dict[str, object],
    candidate_count: int,
    accepted_count: int,
    rejected_count: int,
    candidates: tuple[EvolutionaryCandidate, ...],
    best_candidate: EvolutionaryCandidate | None,
    claim_boundary: str,
    non_actuating: bool,
    execution_disabled: bool,
    hot_patch_permitted: bool,
    live_merge_permitted: bool,
    operator_review_required: bool,
    report_hash: str,
)

Deterministic, offline-only audit report for evolutionary search.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe audit record for review tooling.

Returns

dict[str, object] Return a JSON-safe audit record for review tooling.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_search.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe audit record for review tooling.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe audit record for review tooling.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "generation_count": self.config.generation_count,
        "population_size": self.config.population_size,
        "mutation_step": self.config.mutation_step,
        "minimum_replay_reward": self.config.minimum_replay_reward,
        "minimum_safety_margin": self.config.minimum_safety_margin,
        "parent_policy_hash": self.parent_policy_hash,
        "replay_summary": self.replay_summary,
        "stl_spec": self.stl_spec,
        "stl_monitoring": self.stl_monitoring,
        "candidate_count": self.candidate_count,
        "accepted_count": self.accepted_count,
        "rejected_count": self.rejected_count,
        "best_candidate": self.best_candidate.to_audit_record()
        if self.best_candidate
        else None,
        "candidates": [
            candidate.to_audit_record() for candidate in self.candidates
        ],
        "claim_boundary": self.claim_boundary,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "hot_patch_permitted": self.hot_patch_permitted,
        "live_merge_permitted": self.live_merge_permitted,
        "operator_review_required": self.operator_review_required,
        "report_hash": self.report_hash,
    }

Functions:

run_offline_evolutionary_supervisor_search(
    parent_policy: Mapping[str, object],
    audit_replays: Sequence[Mapping[str, object]],
    *,
    stl_spec: str,
    trace: Mapping[str, Sequence[object]],
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    minimum_replay_reward: float = 0.0,
    minimum_safety_margin: float = 0.0,
) -> EvolutionarySearchReport

Run deterministic offline evolutionary policy mutation search.

Returns review-only candidates plus guards that block any live merge/hot patch.

Parameters

parent_policy : Mapping[str, object] The parent policy genome. audit_replays : Sequence[Mapping[str, object]] Audit replay records used to score candidates. stl_spec : str An STL specification string used as a safety gate. trace : Mapping[str, Sequence[object]] Signal trace keyed by variable name, each a sequence of floats. generation_count : int Number of search generations. population_size : int Number of candidates per generation. mutation_step : float Mutation step size applied per generation. minimum_replay_reward : float Minimum replay reward a candidate must reach. minimum_safety_margin : float Minimum safety margin a candidate must preserve.

Returns

EvolutionarySearchReport The offline evolutionary search report.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_search.py
def run_offline_evolutionary_supervisor_search(
    parent_policy: Mapping[str, object],
    audit_replays: Sequence[Mapping[str, object]],
    *,
    stl_spec: str,
    trace: Mapping[str, Sequence[object]],
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    minimum_replay_reward: float = 0.0,
    minimum_safety_margin: float = 0.0,
) -> EvolutionarySearchReport:
    """Run deterministic offline evolutionary policy mutation search.

    Returns review-only candidates plus guards that block any live merge/hot patch.

    Parameters
    ----------
    parent_policy : Mapping[str, object]
        The parent policy genome.
    audit_replays : Sequence[Mapping[str, object]]
        Audit replay records used to score candidates.
    stl_spec : str
        An STL specification string used as a safety gate.
    trace : Mapping[str, Sequence[object]]
        Signal trace keyed by variable name, each a sequence of floats.
    generation_count : int
        Number of search generations.
    population_size : int
        Number of candidates per generation.
    mutation_step : float
        Mutation step size applied per generation.
    minimum_replay_reward : float
        Minimum replay reward a candidate must reach.
    minimum_safety_margin : float
        Minimum safety margin a candidate must preserve.

    Returns
    -------
    EvolutionarySearchReport
        The offline evolutionary search report.
    """
    config = EvolutionarySearchConfig(
        generation_count=generation_count,
        population_size=population_size,
        mutation_step=mutation_step,
        minimum_replay_reward=minimum_replay_reward,
        minimum_safety_margin=minimum_safety_margin,
    )
    parent = _validate_parent_policy(parent_policy)
    replays = _validate_replays(audit_replays)
    replay_summary = _summarise_replays(replays)
    stl_monitor, stl_result, validated_trace = _validate_and_evaluate_stl(
        stl_spec=stl_spec,
        trace=trace,
    )
    genome_keys = tuple(sorted(parent))

    def _candidate_genome(
        mutated: Mapping[str, float],
    ) -> tuple[tuple[str, float], ...]:
        """Return the mutated candidate genome from the parent."""
        return tuple((key, float(mutated[key])) for key in sorted(mutated))

    candidates: list[EvolutionaryCandidate] = []
    for generation in range(config.generation_count):
        for local_idx in range(config.population_size):
            knob = genome_keys[(generation + local_idx) % len(genome_keys)]
            parent_value = parent[knob]
            direction = 1.0 if (generation + local_idx) % 2 == 0 else -1.0
            magnitude = _deterministic_mutation_magnitude(
                config=config,
                generation=generation,
                local_index=local_idx,
            )
            mutation_delta = direction * magnitude
            mutated_policy = dict(parent)
            mutated_policy[knob] = parent_value + mutation_delta

            blocked = _candidate_blocked_reasons(
                replay_summary,
                stl_result["robustness"],
                mutation_delta=mutation_delta,
                minimum_replay_reward=config.minimum_replay_reward,
                minimum_safety_margin=config.minimum_safety_margin,
            )
            # Candidate fitness is replay-weighted so selection stays offline,
            # replay-summary deterministic, and biased toward smaller drift.
            replay_fitness = float(replay_summary["mean_reward"]) - 0.5 * abs(
                mutation_delta
            )
            candidate = EvolutionaryCandidate(
                candidate_id=f"g{generation + 1:03d}-c{local_idx + 1:03d}",
                generation=generation + 1,
                knob=knob,
                parent_value=parent_value,
                candidate_value=float(mutated_policy[knob]),
                mutation_delta=float(mutation_delta),
                genome=_candidate_genome(mutated_policy),
                replay_fitness=replay_fitness,
                stl_robustness=float(stl_result["robustness"]),
                stl_satisfied=bool(stl_result["satisfied"]),
                replay_violation_count=int(replay_summary["violation_count"]),
                blocked_reasons=tuple(blocked),
                candidate_hash="",
            )
            candidates.append(
                replace(
                    candidate,
                    candidate_hash=_build_stable_hash(candidate.to_audit_record()),
                )
            )

    accepted = [candidate for candidate in candidates if candidate.accepted]
    rejected = [candidate for candidate in candidates if not candidate.accepted]
    best_candidate = max(
        accepted,
        key=lambda candidate: candidate.replay_fitness + candidate.stl_robustness,
        default=None,
    )
    stl_monitor_record = stl_monitor.evaluate_result(validated_trace).to_audit_record()
    report = EvolutionarySearchReport(
        schema_name="evolutionary_supervisor_policy_search",
        schema_version="0.1.0",
        config=config,
        parent_policy_hash=_build_stable_hash(parent),
        replay_summary=replay_summary,
        stl_spec=stl_spec,
        stl_monitoring=stl_monitor_record,
        candidate_count=len(candidates),
        accepted_count=len(accepted),
        rejected_count=len(rejected),
        candidates=tuple(candidates),
        best_candidate=best_candidate,
        claim_boundary="offline_evolutionary_supervisor_review_not_live_actuation",
        non_actuating=True,
        execution_disabled=True,
        hot_patch_permitted=False,
        live_merge_permitted=False,
        operator_review_required=True,
        report_hash="",
    )

    report_record = report.to_audit_record()
    report_hash = _build_stable_hash(report_record)
    return EvolutionarySearchReport(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        config=report.config,
        parent_policy_hash=report.parent_policy_hash,
        replay_summary=report.replay_summary,
        stl_spec=report.stl_spec,
        stl_monitoring=report.stl_monitoring,
        candidate_count=report.candidate_count,
        accepted_count=report.accepted_count,
        rejected_count=report.rejected_count,
        candidates=report.candidates,
        best_candidate=report.best_candidate,
        claim_boundary=report.claim_boundary,
        non_actuating=report.non_actuating,
        execution_disabled=report.execution_disabled,
        hot_patch_permitted=report.hot_patch_permitted,
        live_merge_permitted=report.live_merge_permitted,
        operator_review_required=report.operator_review_required,
        report_hash=report_hash,
    )

evolutionary_topology_grammar

Offline review-only topology mutation grammar.

This module provides deterministic candidates for topology mutation operations. All generated candidates are review-only and include stable audit records.

Classes

TopologyMutationConfig dataclass

TopologyMutationConfig(
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    min_edge_weight: float = 0.0,
    max_edge_weight: float = 10.0,
    edge_add_base_weight: float = 0.4,
    max_add_candidates: int = 16,
)

Knobs used to shape deterministic grammar expansion.

TopologyMutationNode dataclass

TopologyMutationNode(
    node_id: int, community: str | None = None
)

Normalised topology node record.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {"node_id": self.node_id, "community": self.community}

TopologyMutationEdge dataclass

TopologyMutationEdge(
    source: int, target: int, weight: float
)

Normalised pairwise edge record.

Attributes
pair property
pair: tuple[int, int]

Return the canonical undirected edge pair.

Returns

tuple[int, int] Return the canonical undirected edge pair.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "source": self.source,
        "target": self.target,
        "weight": self.weight,
    }

TopologyMutationPlan dataclass

TopologyMutationPlan(
    operation: str,
    node_a: int,
    node_b: int,
    source_weight: float,
    candidate_weight: float | None,
    mutation_delta: float,
    source_communities: tuple[str | None, str | None],
)

One planned grammar mutation.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "operation": self.operation,
        "node_a": self.node_a,
        "node_b": self.node_b,
        "source_weight": self.source_weight,
        "candidate_weight": self.candidate_weight,
        "mutation_delta": self.mutation_delta,
        "source_communities": list(self.source_communities),
    }

TopologyMutationCandidate dataclass

TopologyMutationCandidate(
    candidate_id: str,
    generation: int,
    mutation_index: int,
    source_topology_hash: str,
    plan: TopologyMutationPlan,
    source_edge_count: int,
    candidate_edges: tuple[TopologyMutationEdge, ...],
    blocked_reasons: tuple[str, ...],
    candidate_hash: str,
    operator_review_required: bool = True,
    execution_disabled: bool = True,
    live_merge_permitted: bool = False,
    hot_patch_permitted: bool = False,
    actuation_permitted: bool = False,
)

One review-only topology candidate.

Attributes
accepted property
accepted: bool

Return whether this candidate is accepted for review.

Returns

bool Return whether this candidate is accepted for review.

status property
status: str

Return the review status label for this candidate.

Returns

str Return the review status label for this candidate.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "candidate_id": self.candidate_id,
        "generation": self.generation,
        "mutation_index": self.mutation_index,
        "source_topology_hash": self.source_topology_hash,
        "plan": self.plan.to_audit_record(),
        "source_edge_count": self.source_edge_count,
        "candidate_edges": [
            edge.to_audit_record() for edge in self.candidate_edges
        ],
        "blocked_reasons": list(self.blocked_reasons),
        "candidate_hash": self.candidate_hash,
        "operator_review_required": self.operator_review_required,
        "execution_disabled": self.execution_disabled,
        "live_merge_permitted": self.live_merge_permitted,
        "hot_patch_permitted": self.hot_patch_permitted,
        "actuation_permitted": self.actuation_permitted,
        "status": self.status,
    }

TopologyMutationReport dataclass

TopologyMutationReport(
    schema_name: str,
    schema_version: str,
    config: TopologyMutationConfig,
    source_topology_hash: str,
    node_records: tuple[TopologyMutationNode, ...],
    edge_records: tuple[TopologyMutationEdge, ...],
    candidate_count: int,
    accepted_count: int,
    rejected_count: int,
    candidates: tuple[TopologyMutationCandidate, ...],
    claim_boundary: str,
    operator_review_required: bool,
    non_actuating: bool,
    execution_disabled: bool,
    hot_patch_permitted: bool,
    live_merge_permitted: bool,
    actuation_permitted: bool,
    report_hash: str,
)

Offline-only topology mutation audit report.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "generation_count": self.config.generation_count,
        "population_size": self.config.population_size,
        "mutation_step": self.config.mutation_step,
        "min_edge_weight": self.config.min_edge_weight,
        "max_edge_weight": self.config.max_edge_weight,
        "edge_add_base_weight": self.config.edge_add_base_weight,
        "max_add_candidates": self.config.max_add_candidates,
        "source_topology_hash": self.source_topology_hash,
        "source_nodes": [node.to_audit_record() for node in self.node_records],
        "source_edges": [edge.to_audit_record() for edge in self.edge_records],
        "candidate_count": self.candidate_count,
        "accepted_count": self.accepted_count,
        "rejected_count": self.rejected_count,
        "candidates": [
            candidate.to_audit_record() for candidate in self.candidates
        ],
        "claim_boundary": self.claim_boundary,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "hot_patch_permitted": self.hot_patch_permitted,
        "live_merge_permitted": self.live_merge_permitted,
        "actuation_permitted": self.actuation_permitted,
        "report_hash": self.report_hash,
    }

Functions:

run_offline_evolutionary_topology_mutation_search(
    node_records: Sequence[Mapping[str, object]],
    edge_records: Sequence[Mapping[str, object]],
    *,
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    min_edge_weight: float = 0.0,
    max_edge_weight: float = 10.0,
    edge_add_base_weight: float = 0.4,
    max_add_candidates: int = 16,
) -> TopologyMutationReport

Generate deterministic offline topology mutation candidates.

Parameters

node_records : Sequence[Mapping[str, object]] Topology node records. edge_records : Sequence[Mapping[str, object]] Topology edge records. generation_count : int Number of search generations. population_size : int Number of candidates per generation. mutation_step : float Mutation step size applied per generation. min_edge_weight : float Minimum retained edge weight. max_edge_weight : float Maximum allowed edge weight. edge_add_base_weight : float Base weight assigned to newly added edges. max_add_candidates : int Maximum number of edge-addition candidates.

Returns

TopologyMutationReport The offline topology mutation report.

Raises

ValueError If the node/edge records or bounds are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/evolutionary_topology_grammar.py
def run_offline_evolutionary_topology_mutation_search(
    node_records: Sequence[Mapping[str, object]],
    edge_records: Sequence[Mapping[str, object]],
    *,
    generation_count: int = 2,
    population_size: int = 8,
    mutation_step: float = 0.05,
    min_edge_weight: float = 0.0,
    max_edge_weight: float = 10.0,
    edge_add_base_weight: float = 0.4,
    max_add_candidates: int = 16,
) -> TopologyMutationReport:
    """Generate deterministic offline topology mutation candidates.

    Parameters
    ----------
    node_records : Sequence[Mapping[str, object]]
        Topology node records.
    edge_records : Sequence[Mapping[str, object]]
        Topology edge records.
    generation_count : int
        Number of search generations.
    population_size : int
        Number of candidates per generation.
    mutation_step : float
        Mutation step size applied per generation.
    min_edge_weight : float
        Minimum retained edge weight.
    max_edge_weight : float
        Maximum allowed edge weight.
    edge_add_base_weight : float
        Base weight assigned to newly added edges.
    max_add_candidates : int
        Maximum number of edge-addition candidates.

    Returns
    -------
    TopologyMutationReport
        The offline topology mutation report.

    Raises
    ------
    ValueError
        If the node/edge records or bounds are invalid.
    """
    config = TopologyMutationConfig(
        generation_count=generation_count,
        population_size=population_size,
        mutation_step=mutation_step,
        min_edge_weight=min_edge_weight,
        max_edge_weight=max_edge_weight,
        edge_add_base_weight=edge_add_base_weight,
        max_add_candidates=max_add_candidates,
    )

    nodes = _validate_nodes(node_records)
    edges = _validate_edges(edge_records=edge_records, known_nodes=nodes)

    source_topology_hash = _build_topology_hash(nodes, edges)
    axes = _build_mutation_axes(config=config, nodes=nodes, edges=edges)
    if not axes:
        raise ValueError(
            "topology records do not enable topology mutation axis generation"
        )

    candidates: list[TopologyMutationCandidate] = []
    axis_count = len(axes)
    axis_cursor = 0
    for generation in range(config.generation_count):
        for local_index in range(config.population_size):
            axis = axes[axis_cursor]
            axis_cursor = (axis_cursor + 1) % axis_count
            delta = _deterministic_delta(
                axis_index=axis_cursor,
                generation=generation,
                local_index=local_index,
                mutation_step=config.mutation_step,
            )
            next_edges = {edge.pair: edge.weight for edge in edges}
            blocked_reasons: list[str] = []

            if axis.operation == "edge_reweight":
                candidate_weight = axis.source_weight + delta
                if candidate_weight < config.min_edge_weight:
                    blocked_reasons.append("edge_reweight_below_min_weight")
                if candidate_weight > config.max_edge_weight:
                    blocked_reasons.append("edge_reweight_above_max_weight")
                if not blocked_reasons:
                    next_edges[axis.nodes] = candidate_weight

            elif axis.operation == "edge_remove":
                candidate_weight = 0.0
                if axis.source_weight <= 0.0:
                    blocked_reasons.append("edge_remove_from_zero_weight")
                if not blocked_reasons:
                    next_edges.pop(axis.nodes, None)

            elif axis.operation in ("edge_add", "community_bridge"):
                candidate_weight = config.edge_add_base_weight + abs(delta)
                if candidate_weight < config.min_edge_weight:
                    blocked_reasons.append("edge_add_below_min_weight")
                if candidate_weight > config.max_edge_weight:
                    blocked_reasons.append("edge_add_above_max_weight")
                if not blocked_reasons:
                    next_edges[axis.nodes] = candidate_weight

            else:
                raise ValueError(f"unsupported mutation operation: {axis.operation}")

            mutation_delta = (
                candidate_weight - axis.source_weight
                if candidate_weight is not None
                else -axis.source_weight
            )
            if axis.operation == "edge_remove":
                mutation_delta = -axis.source_weight

            plan = TopologyMutationPlan(
                operation=axis.operation,
                node_a=axis.nodes[0],
                node_b=axis.nodes[1],
                source_weight=axis.source_weight,
                candidate_weight=None
                if axis.operation == "edge_remove"
                else candidate_weight,
                mutation_delta=mutation_delta,
                source_communities=axis.communities,
            )

            mutation_index = generation * config.population_size + local_index + 1
            candidate_id = (
                f"g{generation + 1:03d}-c{local_index + 1:03d}-x{mutation_index:03d}"
            )
            candidate = TopologyMutationCandidate(
                candidate_id=candidate_id,
                generation=generation + 1,
                mutation_index=mutation_index,
                source_topology_hash=source_topology_hash,
                plan=plan,
                source_edge_count=len(edges),
                candidate_edges=_sort_edges(
                    tuple(
                        TopologyMutationEdge(
                            source=pair[0],
                            target=pair[1],
                            weight=weight,
                        )
                        for pair, weight in next_edges.items()
                    )
                ),
                blocked_reasons=tuple(blocked_reasons),
                candidate_hash="",
            )
            candidates.append(
                replace(
                    candidate,
                    candidate_hash=_build_stable_hash(candidate.to_audit_record()),
                )
            )

    accepted = tuple(candidate for candidate in candidates if candidate.accepted)
    rejected = tuple(candidate for candidate in candidates if not candidate.accepted)

    report = TopologyMutationReport(
        schema_name=_SCHEMA_NAME,
        schema_version=_SCHEMA_VERSION,
        config=config,
        source_topology_hash=source_topology_hash,
        node_records=nodes,
        edge_records=edges,
        candidate_count=len(candidates),
        accepted_count=len(accepted),
        rejected_count=len(rejected),
        candidates=tuple(candidates),
        claim_boundary=_CLAIM_BOUNDARY,
        operator_review_required=True,
        non_actuating=True,
        execution_disabled=True,
        hot_patch_permitted=False,
        live_merge_permitted=False,
        actuation_permitted=False,
        report_hash="",
    )

    return TopologyMutationReport(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        config=report.config,
        source_topology_hash=report.source_topology_hash,
        node_records=report.node_records,
        edge_records=report.edge_records,
        candidate_count=report.candidate_count,
        accepted_count=report.accepted_count,
        rejected_count=report.rejected_count,
        candidates=report.candidates,
        claim_boundary=report.claim_boundary,
        operator_review_required=report.operator_review_required,
        non_actuating=report.non_actuating,
        execution_disabled=report.execution_disabled,
        hot_patch_permitted=report.hot_patch_permitted,
        live_merge_permitted=report.live_merge_permitted,
        actuation_permitted=report.actuation_permitted,
        report_hash=_build_stable_hash(report.to_audit_record()),
    )

Federated Review Surfaces

Federated orchestration, differential-privacy noise service, secure aggregation, and transport manifests. These APIs produce audit material and deployment preflight evidence without exporting raw local data.

federated

Review-only federated policy-gradient aggregation manifests.

Classes

FederatedAggregationConfig dataclass

FederatedAggregationConfig(
    clipping_norm: float = 1.0,
    noise_multiplier: float = 1.0,
    epsilon: float = 3.0,
    delta: float = 1e-06,
    min_node_count: int = 3,
)

Privacy and acceptance bounds for one offline aggregation review.

FederatedNodeUpdate dataclass

FederatedNodeUpdate(
    node_id: str,
    policy_delta: tuple[tuple[str, float], ...],
    sample_count: int,
    local_loss: float,
    previous_audit_hash: str,
    privacy_epsilon_spent: float,
    clipped_l2_norm: float,
    clip_scale: float,
    accepted: bool,
    rejection_reasons: tuple[str, ...],
    update_hash: str,
)

Validated node-local policy-gradient update without raw time-series.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe node update evidence.

Returns

dict[str, object] Return JSON-safe node update evidence.

Source code in src/scpn_phase_orchestrator/supervisor/federated.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe node update evidence.

    Returns
    -------
    dict[str, object]
        Return JSON-safe node update evidence.
    """
    return {
        "node_id": self.node_id,
        "policy_delta": [[key, value] for key, value in self.policy_delta],
        "sample_count": self.sample_count,
        "local_loss": self.local_loss,
        "previous_audit_hash": self.previous_audit_hash,
        "privacy_epsilon_spent": self.privacy_epsilon_spent,
        "clipped_l2_norm": self.clipped_l2_norm,
        "clip_scale": self.clip_scale,
        "accepted": self.accepted,
        "rejection_reasons": list(self.rejection_reasons),
        "update_hash": self.update_hash,
    }

FederatedPolicyAggregationReport dataclass

FederatedPolicyAggregationReport(
    schema_name: str,
    schema_version: str,
    config: FederatedAggregationConfig,
    required_policy_keys: tuple[str, ...],
    node_updates: tuple[FederatedNodeUpdate, ...],
    accepted_node_count: int,
    rejected_node_count: int,
    total_sample_count: int,
    aggregate_delta: tuple[tuple[str, float], ...],
    aggregate_hash: str,
    privacy_budget_spent: float,
    privacy_budget_remaining: float,
    raw_time_series_received: bool,
    claim_boundary: str,
    operator_review_required: bool,
    non_actuating: bool,
    execution_disabled: bool,
    live_transport_permitted: bool,
    raw_data_export_permitted: bool,
    actuation_permitted: bool,
    report_hash: str,
)

Offline federated aggregation report with explicit safety boundaries.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe aggregate evidence.

Returns

dict[str, object] Return JSON-safe aggregate evidence.

Source code in src/scpn_phase_orchestrator/supervisor/federated.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe aggregate evidence.

    Returns
    -------
    dict[str, object]
        Return JSON-safe aggregate evidence.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "clipping_norm": self.config.clipping_norm,
        "noise_multiplier": self.config.noise_multiplier,
        "epsilon": self.config.epsilon,
        "delta": self.config.delta,
        "min_node_count": self.config.min_node_count,
        "required_policy_keys": list(self.required_policy_keys),
        "node_updates": [update.to_audit_record() for update in self.node_updates],
        "accepted_node_count": self.accepted_node_count,
        "rejected_node_count": self.rejected_node_count,
        "total_sample_count": self.total_sample_count,
        "aggregate_delta": [[key, value] for key, value in self.aggregate_delta],
        "aggregate_hash": self.aggregate_hash,
        "privacy_budget_spent": self.privacy_budget_spent,
        "privacy_budget_remaining": self.privacy_budget_remaining,
        "raw_time_series_received": self.raw_time_series_received,
        "claim_boundary": self.claim_boundary,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "live_transport_permitted": self.live_transport_permitted,
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "actuation_permitted": self.actuation_permitted,
        "report_hash": self.report_hash,
    }

Functions:

build_federated_meta_orchestrator_manifest

build_federated_meta_orchestrator_manifest(
    node_updates: Sequence[Mapping[str, object]],
    *,
    required_policy_keys: Sequence[str] | None = None,
    clipping_norm: float = 1.0,
    noise_multiplier: float = 1.0,
    epsilon: float = 3.0,
    delta: float = 1e-06,
    min_node_count: int = 3,
) -> FederatedPolicyAggregationReport

Build a deterministic review manifest for federated policy aggregation.

Parameters

node_updates : Sequence[Mapping[str, object]] Federated node update records. required_policy_keys : Sequence[str] | None Policy keys every node update must carry, or None. clipping_norm : float L2 clipping norm applied to each node update. noise_multiplier : float Gaussian noise multiplier for differential privacy. epsilon : float Differential-privacy ε budget. delta : float Differential-privacy δ budget. min_node_count : int Minimum number of participating nodes required.

Returns

FederatedPolicyAggregationReport The federated policy aggregation review manifest.

Raises

ValueError If the node updates or privacy parameters are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated.py
def build_federated_meta_orchestrator_manifest(
    node_updates: Sequence[Mapping[str, object]],
    *,
    required_policy_keys: Sequence[str] | None = None,
    clipping_norm: float = 1.0,
    noise_multiplier: float = 1.0,
    epsilon: float = 3.0,
    delta: float = 1e-6,
    min_node_count: int = 3,
) -> FederatedPolicyAggregationReport:
    """Build a deterministic review manifest for federated policy aggregation.

    Parameters
    ----------
    node_updates : Sequence[Mapping[str, object]]
        Federated node update records.
    required_policy_keys : Sequence[str] | None
        Policy keys every node update must carry, or ``None``.
    clipping_norm : float
        L2 clipping norm applied to each node update.
    noise_multiplier : float
        Gaussian noise multiplier for differential privacy.
    epsilon : float
        Differential-privacy ``ε`` budget.
    delta : float
        Differential-privacy ``δ`` budget.
    min_node_count : int
        Minimum number of participating nodes required.

    Returns
    -------
    FederatedPolicyAggregationReport
        The federated policy aggregation review manifest.

    Raises
    ------
    ValueError
        If the node updates or privacy parameters are invalid.
    """
    config = FederatedAggregationConfig(
        clipping_norm=clipping_norm,
        noise_multiplier=noise_multiplier,
        epsilon=epsilon,
        delta=delta,
        min_node_count=min_node_count,
    )
    if not isinstance(node_updates, Sequence) or isinstance(
        node_updates, (str, bytes, bytearray)
    ):
        raise ValueError("node_updates must be a sequence of mappings")
    if not node_updates:
        raise ValueError("node_updates must be non-empty")
    keys = _required_keys(required_policy_keys, node_updates)
    updates = tuple(
        _validate_node_update(raw, required_policy_keys=keys, config=config)
        for raw in node_updates
    )
    accepted = tuple(update for update in updates if update.accepted)
    rejected = tuple(update for update in updates if not update.accepted)
    if len(accepted) < config.min_node_count:
        aggregate_delta: tuple[tuple[str, float], ...] = tuple(
            (key, 0.0) for key in keys
        )
        total_samples = 0
    else:
        aggregate_delta, total_samples = _weighted_average(accepted, keys)
    aggregate_hash = _stable_hash(
        {
            "required_policy_keys": list(keys),
            "aggregate_delta": [[key, value] for key, value in aggregate_delta],
            "accepted_node_ids": [update.node_id for update in accepted],
        }
    )
    privacy_spent = max(
        (update.privacy_epsilon_spent for update in accepted), default=0.0
    )
    report = FederatedPolicyAggregationReport(
        schema_name="federated_meta_orchestrator_policy_aggregation",
        schema_version="0.1.0",
        config=config,
        required_policy_keys=keys,
        node_updates=updates,
        accepted_node_count=len(accepted),
        rejected_node_count=len(rejected),
        total_sample_count=total_samples,
        aggregate_delta=aggregate_delta,
        aggregate_hash=aggregate_hash,
        privacy_budget_spent=privacy_spent,
        privacy_budget_remaining=max(0.0, config.epsilon - privacy_spent),
        raw_time_series_received=False,
        claim_boundary="federated_meta_orchestrator_review_not_live_transport",
        operator_review_required=True,
        non_actuating=True,
        execution_disabled=True,
        live_transport_permitted=False,
        raw_data_export_permitted=False,
        actuation_permitted=False,
        report_hash="",
    )
    return FederatedPolicyAggregationReport(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        config=report.config,
        required_policy_keys=report.required_policy_keys,
        node_updates=report.node_updates,
        accepted_node_count=report.accepted_node_count,
        rejected_node_count=report.rejected_node_count,
        total_sample_count=report.total_sample_count,
        aggregate_delta=report.aggregate_delta,
        aggregate_hash=report.aggregate_hash,
        privacy_budget_spent=report.privacy_budget_spent,
        privacy_budget_remaining=report.privacy_budget_remaining,
        raw_time_series_received=report.raw_time_series_received,
        claim_boundary=report.claim_boundary,
        operator_review_required=report.operator_review_required,
        non_actuating=report.non_actuating,
        execution_disabled=report.execution_disabled,
        live_transport_permitted=report.live_transport_permitted,
        raw_data_export_permitted=report.raw_data_export_permitted,
        actuation_permitted=report.actuation_permitted,
        report_hash=_stable_hash(report.to_audit_record()),
    )

federated_dp_noise_service

Offline differential-privacy noise service manifests for review-only use.

Classes

DpNoiseServiceReadiness dataclass

DpNoiseServiceReadiness(ready: bool, reason: str)

Review readiness for the offline audit service.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {"ready": self.ready, "reason": self.reason}

DpNoiseNodePrivacyBudget dataclass

DpNoiseNodePrivacyBudget(
    node_id: str, epsilon_spent: float
)

Per-node privacy spend declared for DP budget accounting.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {"node_id": self.node_id, "epsilon_spent": self.epsilon_spent}

DpNoiseServiceRequestManifest dataclass

DpNoiseServiceRequestManifest(
    epsilon: float,
    delta: float,
    sensitivity: float,
    noise_multiplier: float,
    node_count: int,
    seed_hash: str,
    policy_keys: tuple[str, ...],
    node_budgets: tuple[DpNoiseNodePrivacyBudget, ...],
    schema_name: str = "federated_dp_noise_service",
    schema_version: str = "1.0.0",
)

Validated request for offline DP-noise boundary review.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "epsilon": self.epsilon,
        "delta": self.delta,
        "sensitivity": self.sensitivity,
        "noise_multiplier": self.noise_multiplier,
        "node_count": self.node_count,
        "seed_hash": self.seed_hash,
        "policy_keys": list(self.policy_keys),
        "node_budgets": [budget.to_audit_record() for budget in self.node_budgets],
    }

DpNoiseServiceResponseManifest dataclass

DpNoiseServiceResponseManifest(
    schema_name: str,
    schema_version: str,
    request_hash: str,
    service_readiness: DpNoiseServiceReadiness,
    epsilon: float,
    delta: float,
    sensitivity: float,
    noise_multiplier: float,
    privacy_budget_spent: float,
    privacy_budget_remaining: float,
    node_count: int,
    policy_keys: tuple[str, ...],
    policy_noise_audit_vector: tuple[
        tuple[str, float], ...
    ],
    service_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    node_budgets: tuple[DpNoiseNodePrivacyBudget, ...],
    audit_record_hash: str,
)

Offline review manifest returned by the audit boundary.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "request_hash": self.request_hash,
        "service_readiness": self.service_readiness.to_audit_record(),
        "epsilon": self.epsilon,
        "delta": self.delta,
        "sensitivity": self.sensitivity,
        "noise_multiplier": self.noise_multiplier,
        "privacy_budget_spent": self.privacy_budget_spent,
        "privacy_budget_remaining": self.privacy_budget_remaining,
        "node_count": self.node_count,
        "policy_keys": list(self.policy_keys),
        "policy_noise_audit_vector": [
            [key, value] for key, value in self.policy_noise_audit_vector
        ],
        "service_execution_permitted": self.service_execution_permitted,
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "node_budgets": [budget.to_audit_record() for budget in self.node_budgets],
        "audit_record_hash": self.audit_record_hash,
    }

DpNoiseServiceDeploymentPreflightManifest dataclass

DpNoiseServiceDeploymentPreflightManifest(
    schema_name: str,
    schema_version: str,
    mechanism_label: str,
    privacy_accountant_owner: str,
    seed_custody_label: str,
    budget_issuer_label: str,
    service_endpoint_label: str,
    operator_approved: bool,
    request_hash: str,
    response_hash: str,
    epsilon: float,
    delta: float,
    deployment_readiness: DpNoiseServiceReadiness,
    service_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    audit_record_hash: str,
)

Deterministic review-only deployment preflight manifest.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "mechanism_label": self.mechanism_label,
        "privacy_accountant_owner": self.privacy_accountant_owner,
        "seed_custody_label": self.seed_custody_label,
        "budget_issuer_label": self.budget_issuer_label,
        "service_endpoint_label": self.service_endpoint_label,
        "operator_approved": self.operator_approved,
        "request_hash": self.request_hash,
        "response_hash": self.response_hash,
        "epsilon": self.epsilon,
        "delta": self.delta,
        "deployment_readiness": self.deployment_readiness.to_audit_record(),
        "service_execution_permitted": self.service_execution_permitted,
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "audit_record_hash": self.audit_record_hash,
    }

Functions:

build_dp_noise_service_manifest

build_dp_noise_service_manifest(
    request: DpNoiseServiceRequestManifest,
) -> DpNoiseServiceResponseManifest

Build a deterministic, dependency-free offline DP-noise review manifest.

Parameters

request : DpNoiseServiceRequestManifest The DP-noise service request manifest.

Returns

DpNoiseServiceResponseManifest The offline DP-noise review response manifest.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def build_dp_noise_service_manifest(
    request: DpNoiseServiceRequestManifest,
) -> DpNoiseServiceResponseManifest:
    """Build a deterministic, dependency-free offline DP-noise review manifest.

    Parameters
    ----------
    request : DpNoiseServiceRequestManifest
        The DP-noise service request manifest.

    Returns
    -------
    DpNoiseServiceResponseManifest
        The offline DP-noise review response manifest.
    """
    request_hash = _stable_hash(request.to_audit_record())
    policy_noise_audit_vector = _generate_audit_noise(
        request.seed_hash,
        request.policy_keys,
        request.sensitivity,
        request.noise_multiplier,
    )
    privacy_budget_spent = round(
        sum(budget.epsilon_spent for budget in request.node_budgets), 12
    )
    privacy_budget_remaining = round(request.epsilon - privacy_budget_spent, 12)
    response = DpNoiseServiceResponseManifest(
        schema_name=request.schema_name,
        schema_version=request.schema_version,
        request_hash=request_hash,
        service_readiness=DpNoiseServiceReadiness(
            ready=True, reason="offline_review_only_manifest_ready"
        ),
        epsilon=request.epsilon,
        delta=request.delta,
        sensitivity=request.sensitivity,
        noise_multiplier=request.noise_multiplier,
        privacy_budget_spent=privacy_budget_spent,
        privacy_budget_remaining=privacy_budget_remaining,
        node_count=request.node_count,
        policy_keys=request.policy_keys,
        policy_noise_audit_vector=policy_noise_audit_vector,
        service_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        node_budgets=request.node_budgets,
        audit_record_hash="",
    )
    return DpNoiseServiceResponseManifest(
        schema_name=response.schema_name,
        schema_version=response.schema_version,
        request_hash=response.request_hash,
        service_readiness=response.service_readiness,
        epsilon=response.epsilon,
        delta=response.delta,
        sensitivity=response.sensitivity,
        noise_multiplier=response.noise_multiplier,
        privacy_budget_spent=response.privacy_budget_spent,
        privacy_budget_remaining=response.privacy_budget_remaining,
        node_count=response.node_count,
        policy_keys=response.policy_keys,
        policy_noise_audit_vector=response.policy_noise_audit_vector,
        service_execution_permitted=response.service_execution_permitted,
        raw_data_export_permitted=response.raw_data_export_permitted,
        operator_review_required=response.operator_review_required,
        non_actuating=response.non_actuating,
        node_budgets=response.node_budgets,
        audit_record_hash=_stable_hash(response.to_audit_record()),
    )

build_dp_noise_service_deployment_preflight_manifest

build_dp_noise_service_deployment_preflight_manifest(
    request_manifest: DpNoiseServiceRequestManifest,
    response_manifest: DpNoiseServiceResponseManifest,
    *,
    mechanism_label: str,
    privacy_accountant_owner: str,
    seed_custody_label: str,
    budget_issuer_label: str,
    service_endpoint_label: str,
    operator_approved: bool,
) -> DpNoiseServiceDeploymentPreflightManifest

Build a deterministic DP-noise deployment preflight manifest.

Parameters

request_manifest : DpNoiseServiceRequestManifest The DP-noise service request manifest. response_manifest : DpNoiseServiceResponseManifest The DP-noise service response manifest. mechanism_label : str Label of the privacy mechanism. privacy_accountant_owner : str Owner of the privacy accountant. seed_custody_label : str Label describing seed custody. budget_issuer_label : str Label of the privacy-budget issuer. service_endpoint_label : str Label of the service endpoint. operator_approved : bool Whether a human operator has approved the deployment.

Returns

DpNoiseServiceDeploymentPreflightManifest The DP-noise deployment preflight manifest.

Raises

ValueError If the request and response manifests are inconsistent.

Source code in src/scpn_phase_orchestrator/supervisor/federated_dp_noise_service.py
def build_dp_noise_service_deployment_preflight_manifest(
    request_manifest: DpNoiseServiceRequestManifest,
    response_manifest: DpNoiseServiceResponseManifest,
    *,
    mechanism_label: str,
    privacy_accountant_owner: str,
    seed_custody_label: str,
    budget_issuer_label: str,
    service_endpoint_label: str,
    operator_approved: bool,
) -> DpNoiseServiceDeploymentPreflightManifest:
    """Build a deterministic DP-noise deployment preflight manifest.

    Parameters
    ----------
    request_manifest : DpNoiseServiceRequestManifest
        The DP-noise service request manifest.
    response_manifest : DpNoiseServiceResponseManifest
        The DP-noise service response manifest.
    mechanism_label : str
        Label of the privacy mechanism.
    privacy_accountant_owner : str
        Owner of the privacy accountant.
    seed_custody_label : str
        Label describing seed custody.
    budget_issuer_label : str
        Label of the privacy-budget issuer.
    service_endpoint_label : str
        Label of the service endpoint.
    operator_approved : bool
        Whether a human operator has approved the deployment.

    Returns
    -------
    DpNoiseServiceDeploymentPreflightManifest
        The DP-noise deployment preflight manifest.

    Raises
    ------
    ValueError
        If the request and response manifests are inconsistent.
    """
    if not isinstance(request_manifest, DpNoiseServiceRequestManifest):
        raise ValueError("request_manifest must be a DpNoiseServiceRequestManifest")
    if not isinstance(response_manifest, DpNoiseServiceResponseManifest):
        raise ValueError("response_manifest must be a DpNoiseServiceResponseManifest")

    request_hash = _stable_hash(request_manifest.to_audit_record())
    response_hash_record = response_manifest.to_audit_record()
    response_hash_record["audit_record_hash"] = ""
    response_hash = _stable_hash(response_hash_record)

    missing_reasons: list[str] = []

    mechanism_label = _validated_label(
        mechanism_label, "mechanism_label", missing_reasons
    )
    privacy_accountant_owner = _validated_label(
        privacy_accountant_owner,
        "privacy_accountant_owner",
        missing_reasons,
    )
    seed_custody_label = _validated_label(
        seed_custody_label, "seed_custody_label", missing_reasons
    )
    budget_issuer_label = _validated_label(
        budget_issuer_label, "budget_issuer_label", missing_reasons
    )
    service_endpoint_label = _validated_label(
        service_endpoint_label, "service_endpoint_label", missing_reasons
    )
    if not isinstance(operator_approved, bool):
        raise ValueError("operator_approved must be a boolean")
    if operator_approved is False:
        missing_reasons.append("operator approval required")

    if response_manifest.request_hash != request_hash:
        missing_reasons.append("request and response hash linkage broken")
    if response_manifest.audit_record_hash != response_hash:
        missing_reasons.append("response hash integrity check failed")

    if request_manifest.epsilon != response_manifest.epsilon:
        missing_reasons.append(
            "epsilon mismatch between request and response manifests"
        )
    if request_manifest.delta != response_manifest.delta:
        missing_reasons.append("delta mismatch between request and response manifests")

    if not math.isfinite(response_manifest.epsilon) or response_manifest.epsilon <= 0.0:
        missing_reasons.append("response epsilon must be finite and positive")
    if (
        not math.isfinite(response_manifest.delta)
        or not 0.0 < response_manifest.delta < 1.0
    ):
        missing_reasons.append("response delta must be finite in (0, 1)")

    ready = len(missing_reasons) == 0
    reason = (
        "offline_deployment_preflight_ready" if ready else "; ".join(missing_reasons)
    )
    manifest = DpNoiseServiceDeploymentPreflightManifest(
        schema_name="federated_dp_noise_service_deployment_preflight_manifest",
        schema_version="1.0.0",
        mechanism_label=mechanism_label.strip(),
        privacy_accountant_owner=privacy_accountant_owner.strip(),
        seed_custody_label=seed_custody_label.strip(),
        budget_issuer_label=budget_issuer_label.strip(),
        service_endpoint_label=service_endpoint_label.strip(),
        operator_approved=operator_approved,
        request_hash=request_hash,
        response_hash=response_hash,
        epsilon=request_manifest.epsilon,
        delta=request_manifest.delta,
        deployment_readiness=DpNoiseServiceReadiness(ready=ready, reason=reason),
        service_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        audit_record_hash="",
    )
    return DpNoiseServiceDeploymentPreflightManifest(
        schema_name=manifest.schema_name,
        schema_version=manifest.schema_version,
        mechanism_label=manifest.mechanism_label,
        privacy_accountant_owner=manifest.privacy_accountant_owner,
        seed_custody_label=manifest.seed_custody_label,
        budget_issuer_label=manifest.budget_issuer_label,
        service_endpoint_label=manifest.service_endpoint_label,
        operator_approved=manifest.operator_approved,
        request_hash=manifest.request_hash,
        response_hash=manifest.response_hash,
        epsilon=manifest.epsilon,
        delta=manifest.delta,
        deployment_readiness=manifest.deployment_readiness,
        service_execution_permitted=manifest.service_execution_permitted,
        raw_data_export_permitted=manifest.raw_data_export_permitted,
        operator_review_required=manifest.operator_review_required,
        non_actuating=manifest.non_actuating,
        audit_record_hash=_stable_hash(manifest.to_audit_record()),
    )

federated_secure_aggregation

Offline secure aggregation manifests for federated supervisor review.

Classes

SecureAggregationConfig dataclass

SecureAggregationConfig(
    clipping_norm: float = 1.0,
    min_node_count: int = 3,
    epsilon: float = 3.0,
    delta: float = 1e-06,
)

Offline policy for deterministic manifest-only secure aggregation.

SecureNodeCommitment dataclass

SecureNodeCommitment(
    node_id: str,
    masked_policy_delta: tuple[tuple[str, float], ...],
    sample_count: int,
    share_commitment: str,
    share_commitment_hash: str,
    share_hash: str,
    masked_delta_hash: str,
    accepted: bool,
    rejection_reasons: tuple[str, ...],
    update_hash: str,
)

Validated masked node commitment used in secure aggregation review.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe audit record for this node commitment.

Returns

dict[str, object] Return a JSON-safe audit record for this node commitment.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe audit record for this node commitment.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe audit record for this node commitment.
    """
    return {
        "node_id": self.node_id,
        "masked_policy_delta": list(self.masked_policy_delta),
        "sample_count": self.sample_count,
        "share_commitment": self.share_commitment,
        "share_commitment_hash": self.share_commitment_hash,
        "share_hash": self.share_hash,
        "masked_delta_hash": self.masked_delta_hash,
        "accepted": self.accepted,
        "rejection_reasons": list(self.rejection_reasons),
        "update_hash": self.update_hash,
    }

SecureAggregationQuorumEvidence dataclass

SecureAggregationQuorumEvidence(
    node_id: str, evidence_hash: str
)

Signed quorum metadata for review-only preflight.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe quorum evidence record.

Returns

dict[str, object] Return a JSON-safe quorum evidence record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe quorum evidence record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe quorum evidence record.
    """
    return {"node_id": self.node_id, "evidence_hash": self.evidence_hash}

SecureNodeCustodyRecord dataclass

SecureNodeCustodyRecord(
    node_id: str,
    key_custody_label: str,
    share_custody_label: str,
    previous_key_custody_label: str,
    previous_share_custody_label: str,
    key_custody_continuity_hash: str,
    share_custody_continuity_hash: str,
)

Custody metadata for key and share labels used in review.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe node-custody audit record.

Returns

dict[str, object] Return a JSON-safe node-custody audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe node-custody audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe node-custody audit record.
    """
    return {
        "node_id": self.node_id,
        "key_custody_label": self.key_custody_label,
        "share_custody_label": self.share_custody_label,
        "previous_key_custody_label": self.previous_key_custody_label,
        "previous_share_custody_label": self.previous_share_custody_label,
        "key_custody_continuity_hash": self.key_custody_continuity_hash,
        "share_custody_continuity_hash": self.share_custody_continuity_hash,
    }

FederatedSecureAggregationManifest dataclass

FederatedSecureAggregationManifest(
    schema_name: str,
    schema_version: str,
    config: SecureAggregationConfig,
    required_policy_keys: tuple[str, ...],
    node_commitments: tuple[SecureNodeCommitment, ...],
    accepted_node_count: int,
    rejected_node_count: int,
    total_sample_count: int,
    aggregate_masked_delta: tuple[tuple[str, float], ...],
    aggregate_masked_delta_hash: str,
    secure_aggregation_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    quorum_met: bool,
    claim_boundary: str,
    report_hash: str,
)

Manifest for deterministic offline secure aggregation review only.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe aggregate audit record.

Returns

dict[str, object] Return a JSON-safe aggregate audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe aggregate audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe aggregate audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "clipping_norm": self.config.clipping_norm,
        "min_node_count": self.config.min_node_count,
        "epsilon": self.config.epsilon,
        "delta": self.config.delta,
        "required_policy_keys": list(self.required_policy_keys),
        "node_commitments": [
            node.to_audit_record() for node in self.node_commitments
        ],
        "accepted_node_count": self.accepted_node_count,
        "rejected_node_count": self.rejected_node_count,
        "total_sample_count": self.total_sample_count,
        "aggregate_masked_delta": list(self.aggregate_masked_delta),
        "aggregate_masked_delta_hash": self.aggregate_masked_delta_hash,
        "secure_aggregation_execution_permitted": (
            self.secure_aggregation_execution_permitted
        ),
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "quorum_met": self.quorum_met,
        "claim_boundary": self.claim_boundary,
        "report_hash": self.report_hash,
    }

FederatedSecureAggregationPreflightManifest dataclass

FederatedSecureAggregationPreflightManifest(
    schema_name: str,
    schema_version: str,
    secure_aggregation_schema_name: str,
    secure_aggregation_schema_version: str,
    secure_aggregation_report_hash: str,
    accepted_node_threshold: int,
    accepted_node_count: int,
    quorum_evidence: tuple[
        SecureAggregationQuorumEvidence, ...
    ],
    custody_rotation_policy: str,
    custody_records: tuple[SecureNodeCustodyRecord, ...],
    operator_approved: bool,
    operator_id: str,
    service_owner: str,
    secure_aggregation_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    report_hash: str,
)

Review-only deployment preflight envelope for manifest execution.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe preflight audit record.

Returns

dict[str, object] Return a JSON-safe preflight audit record.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe preflight audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe preflight audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "secure_aggregation_schema_name": self.secure_aggregation_schema_name,
        "secure_aggregation_schema_version": self.secure_aggregation_schema_version,
        "secure_aggregation_report_hash": self.secure_aggregation_report_hash,
        "accepted_node_threshold": self.accepted_node_threshold,
        "accepted_node_count": self.accepted_node_count,
        "quorum_evidence": [
            entry.to_audit_record() for entry in self.quorum_evidence
        ],
        "custody_rotation_policy": self.custody_rotation_policy,
        "custody_records": [
            record.to_audit_record() for record in self.custody_records
        ],
        "operator_approved": self.operator_approved,
        "operator_id": self.operator_id,
        "service_owner": self.service_owner,
        "secure_aggregation_execution_permitted": (
            self.secure_aggregation_execution_permitted
        ),
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "report_hash": self.report_hash,
    }

Functions:

build_federated_secure_aggregation_manifest

build_federated_secure_aggregation_manifest(
    node_commitments: Sequence[Mapping[str, object]],
    *,
    required_policy_keys: Sequence[str] | None = None,
    clipping_norm: float = 1.0,
    min_node_count: int = 3,
    epsilon: float = 3.0,
    delta: float = 1e-06,
) -> FederatedSecureAggregationManifest

Build a deterministic secure aggregation manifest.

Parameters

node_commitments : Sequence[Mapping[str, object]] Secure-aggregation node commitment records. required_policy_keys : Sequence[str] | None Policy keys every node update must carry, or None. clipping_norm : float L2 clipping norm applied to each node update. min_node_count : int Minimum number of participating nodes required. epsilon : float Differential-privacy ε budget. delta : float Differential-privacy δ budget.

Returns

FederatedSecureAggregationManifest The secure-aggregation manifest.

Raises

ValueError If the node commitments or privacy parameters are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def build_federated_secure_aggregation_manifest(
    node_commitments: Sequence[Mapping[str, object]],
    *,
    required_policy_keys: Sequence[str] | None = None,
    clipping_norm: float = 1.0,
    min_node_count: int = 3,
    epsilon: float = 3.0,
    delta: float = 1e-6,
) -> FederatedSecureAggregationManifest:
    """Build a deterministic secure aggregation manifest.

    Parameters
    ----------
    node_commitments : Sequence[Mapping[str, object]]
        Secure-aggregation node commitment records.
    required_policy_keys : Sequence[str] | None
        Policy keys every node update must carry, or ``None``.
    clipping_norm : float
        L2 clipping norm applied to each node update.
    min_node_count : int
        Minimum number of participating nodes required.
    epsilon : float
        Differential-privacy ``ε`` budget.
    delta : float
        Differential-privacy ``δ`` budget.

    Returns
    -------
    FederatedSecureAggregationManifest
        The secure-aggregation manifest.

    Raises
    ------
    ValueError
        If the node commitments or privacy parameters are invalid.
    """
    config = SecureAggregationConfig(
        clipping_norm=clipping_norm,
        min_node_count=min_node_count,
        epsilon=epsilon,
        delta=delta,
    )
    if not isinstance(node_commitments, Sequence) or isinstance(
        node_commitments, (str, bytes, bytearray)
    ):
        raise ValueError("node_commitments must be a sequence of mappings")
    if not node_commitments:
        raise ValueError("node_commitments must be non-empty")

    keys = _resolve_required_policy_keys(required_policy_keys, node_commitments)
    seen_node_ids: set[str] = set()
    validated = tuple(
        _validate_node_commitment(
            raw,
            required_policy_keys=keys,
            config=config,
            seen_node_ids=seen_node_ids,
        )
        for raw in node_commitments
    )
    validated = tuple(sorted(validated, key=lambda entry: entry.node_id))
    accepted = tuple(node for node in validated if node.accepted)
    rejected = tuple(node for node in validated if not node.accepted)

    if len(accepted) < config.min_node_count:
        raise ValueError(
            "quorum_not_met: accepted node commitments below minimum node count"
        )

    aggregate_masked_delta, total_sample_count = _weighted_masked_average(
        accepted, keys
    )
    aggregate_masked_delta_hash = _stable_hash(
        {
            "aggregate_masked_delta": list(aggregate_masked_delta),
            "accepted_node_ids": sorted(node.node_id for node in accepted),
            "required_policy_keys": list(keys),
        }
    )

    report = FederatedSecureAggregationManifest(
        schema_name="federated_secure_aggregation_manifest",
        schema_version="0.1.0",
        config=config,
        required_policy_keys=keys,
        node_commitments=validated,
        accepted_node_count=len(accepted),
        rejected_node_count=len(rejected),
        total_sample_count=total_sample_count,
        aggregate_masked_delta=aggregate_masked_delta,
        aggregate_masked_delta_hash=aggregate_masked_delta_hash,
        secure_aggregation_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        quorum_met=True,
        claim_boundary="offline_review_only_no_live_transport",
        report_hash="",
    )
    return FederatedSecureAggregationManifest(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        config=report.config,
        required_policy_keys=report.required_policy_keys,
        node_commitments=report.node_commitments,
        accepted_node_count=report.accepted_node_count,
        rejected_node_count=report.rejected_node_count,
        total_sample_count=report.total_sample_count,
        aggregate_masked_delta=report.aggregate_masked_delta,
        aggregate_masked_delta_hash=report.aggregate_masked_delta_hash,
        secure_aggregation_execution_permitted=report.secure_aggregation_execution_permitted,
        raw_data_export_permitted=report.raw_data_export_permitted,
        operator_review_required=report.operator_review_required,
        non_actuating=report.non_actuating,
        quorum_met=report.quorum_met,
        claim_boundary=report.claim_boundary,
        report_hash=_stable_hash(report.to_audit_record()),
    )

build_federated_secure_aggregation_preflight_manifest

build_federated_secure_aggregation_preflight_manifest(
    secure_aggregation_manifest: FederatedSecureAggregationManifest,
    *,
    quorum_evidence: Sequence[Mapping[str, object]],
    custody_rotation_policy: str,
    custody_records: Sequence[Mapping[str, object]],
    accepted_node_threshold: int,
    operator_approved: bool,
    operator_id: str,
    service_owner: str,
) -> FederatedSecureAggregationPreflightManifest

Build a deterministic review-only deployment preflight manifest.

Parameters

secure_aggregation_manifest : FederatedSecureAggregationManifest The secure-aggregation manifest to preflight. quorum_evidence : Sequence[Mapping[str, object]] Per-node quorum evidence records. custody_rotation_policy : str Key-custody rotation policy label. custody_records : Sequence[Mapping[str, object]] Node custody records. accepted_node_threshold : int Minimum number of accepted nodes required. operator_approved : bool Whether a human operator has approved the deployment. operator_id : str Identifier of the approving operator. service_owner : str Owner of the aggregation service.

Returns

FederatedSecureAggregationPreflightManifest The secure-aggregation deployment preflight manifest.

Raises

TypeError If an argument has the wrong type. ValueError If the manifest or quorum evidence is invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated_secure_aggregation.py
def build_federated_secure_aggregation_preflight_manifest(
    secure_aggregation_manifest: FederatedSecureAggregationManifest,
    *,
    quorum_evidence: Sequence[Mapping[str, object]],
    custody_rotation_policy: str,
    custody_records: Sequence[Mapping[str, object]],
    accepted_node_threshold: int,
    operator_approved: bool,
    operator_id: str,
    service_owner: str,
) -> FederatedSecureAggregationPreflightManifest:
    """Build a deterministic review-only deployment preflight manifest.

    Parameters
    ----------
    secure_aggregation_manifest : FederatedSecureAggregationManifest
        The secure-aggregation manifest to preflight.
    quorum_evidence : Sequence[Mapping[str, object]]
        Per-node quorum evidence records.
    custody_rotation_policy : str
        Key-custody rotation policy label.
    custody_records : Sequence[Mapping[str, object]]
        Node custody records.
    accepted_node_threshold : int
        Minimum number of accepted nodes required.
    operator_approved : bool
        Whether a human operator has approved the deployment.
    operator_id : str
        Identifier of the approving operator.
    service_owner : str
        Owner of the aggregation service.

    Returns
    -------
    FederatedSecureAggregationPreflightManifest
        The secure-aggregation deployment preflight manifest.

    Raises
    ------
    TypeError
        If an argument has the wrong type.
    ValueError
        If the manifest or quorum evidence is invalid.
    """
    if not isinstance(secure_aggregation_manifest, FederatedSecureAggregationManifest):
        raise TypeError(
            "secure_aggregation_manifest must be a FederatedSecureAggregationManifest"
        )

    if not operator_approved:
        raise ValueError("operator approval is required for preflight")

    operator = _non_empty_text(operator_id, "operator_id")
    owner = _non_empty_text(service_owner, "service_owner")
    threshold = _positive_int(accepted_node_threshold, "accepted_node_threshold")
    rotation_policy = _non_empty_text(
        custody_rotation_policy, "custody_rotation_policy"
    )
    if rotation_policy not in SUPPORTED_CUSTODY_ROTATION_POLICIES:
        raise ValueError("unsupported custody rotation policy")

    if not secure_aggregation_manifest.quorum_met:
        raise ValueError("secure aggregation manifest quorum not met")

    report_record = secure_aggregation_manifest.to_audit_record()
    report_record["report_hash"] = ""
    expected_report_hash = _stable_hash(report_record)
    manifest_report_hash = _sha256_hex(
        secure_aggregation_manifest.report_hash, "secure_aggregation_report_hash"
    )
    if manifest_report_hash != expected_report_hash:
        raise ValueError("secure_aggregation_manifest report hash mismatch")

    accepted_nodes = tuple(
        node.node_id
        for node in secure_aggregation_manifest.node_commitments
        if node.accepted
    )
    if len(accepted_nodes) < threshold:
        raise ValueError("accepted-node threshold not met")

    evidence = _resolve_preflight_quorum_evidence(
        quorum_evidence,
        accepted_nodes=accepted_nodes,
        accepted_node_threshold=threshold,
    )
    custody = _resolve_node_custody_records(
        custody_records,
        accepted_nodes=accepted_nodes,
        custody_rotation_policy=rotation_policy,
    )

    custody_node_ids = tuple(record.node_id for record in custody)
    if len(custody_node_ids) != len(set(custody_node_ids)):
        raise ValueError("custody records must be unique per accepted node")
    if set(custody_node_ids) != set(accepted_nodes):
        raise ValueError("custody labels must cover all accepted nodes")

    preflight = FederatedSecureAggregationPreflightManifest(
        schema_name="federated_secure_aggregation_preflight_manifest",
        schema_version="0.1.0",
        secure_aggregation_schema_name=secure_aggregation_manifest.schema_name,
        secure_aggregation_schema_version=secure_aggregation_manifest.schema_version,
        secure_aggregation_report_hash=manifest_report_hash,
        accepted_node_threshold=threshold,
        accepted_node_count=len(accepted_nodes),
        quorum_evidence=tuple(sorted(evidence, key=lambda entry: entry.node_id)),
        custody_rotation_policy=rotation_policy,
        custody_records=tuple(sorted(custody, key=lambda entry: entry.node_id)),
        operator_approved=operator_approved,
        operator_id=operator,
        service_owner=owner,
        secure_aggregation_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        report_hash="",
    )
    return FederatedSecureAggregationPreflightManifest(
        schema_name=preflight.schema_name,
        schema_version=preflight.schema_version,
        secure_aggregation_schema_name=preflight.secure_aggregation_schema_name,
        secure_aggregation_schema_version=preflight.secure_aggregation_schema_version,
        secure_aggregation_report_hash=preflight.secure_aggregation_report_hash,
        accepted_node_threshold=preflight.accepted_node_threshold,
        accepted_node_count=preflight.accepted_node_count,
        quorum_evidence=preflight.quorum_evidence,
        custody_rotation_policy=preflight.custody_rotation_policy,
        custody_records=preflight.custody_records,
        operator_approved=preflight.operator_approved,
        operator_id=preflight.operator_id,
        service_owner=preflight.service_owner,
        secure_aggregation_execution_permitted=preflight.secure_aggregation_execution_permitted,
        raw_data_export_permitted=preflight.raw_data_export_permitted,
        operator_review_required=preflight.operator_review_required,
        non_actuating=preflight.non_actuating,
        report_hash=_stable_hash(preflight.to_audit_record()),
    )

federated_transport

Federated transport envelope and replay validation helpers.

Classes

FederatedTransportEnvelope dataclass

FederatedTransportEnvelope(
    schema_name: str,
    schema_version: str,
    batch_id: str,
    sequence_position: int,
    node_id: str,
    node_sequence: int,
    envelope_id: str,
    parent_envelope_hash: str,
    node_update_audit_record: tuple[
        tuple[str, object], ...
    ],
    node_update_audit_hash: str,
    envelope_signature: str,
    envelope_hash: str,
    transport_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
)

Signed/hash-linked transport envelope around one node audit update.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe audit evidence for this transport envelope.

Returns

dict[str, object] Return JSON-safe audit evidence for this transport envelope.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe audit evidence for this transport envelope.

    Returns
    -------
    dict[str, object]
        Return JSON-safe audit evidence for this transport envelope.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "batch_id": self.batch_id,
        "sequence_position": self.sequence_position,
        "node_id": self.node_id,
        "node_sequence": self.node_sequence,
        "envelope_id": self.envelope_id,
        "parent_envelope_hash": self.parent_envelope_hash,
        "node_update_audit_record": [
            [key, value] for key, value in self.node_update_audit_record
        ],
        "node_update_audit_hash": self.node_update_audit_hash,
        "envelope_signature": self.envelope_signature,
        "envelope_hash": self.envelope_hash,
        "transport_execution_permitted": self.transport_execution_permitted,
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
    }

FederatedTransportReplayLedger dataclass

FederatedTransportReplayLedger(
    schema_name: str,
    schema_version: str,
    batch_id: str,
    envelope_count: int,
    envelope_ids: tuple[str, ...],
    node_last_sequences: tuple[tuple[str, int], ...],
    replay_hash: str,
)

Replay result for an ordered transport batch.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe replay evidence.

Returns

dict[str, object] Return JSON-safe replay evidence.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe replay evidence.

    Returns
    -------
    dict[str, object]
        Return JSON-safe replay evidence.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "batch_id": self.batch_id,
        "envelope_count": self.envelope_count,
        "envelope_ids": list(self.envelope_ids),
        "node_last_sequences": [list(pair) for pair in self.node_last_sequences],
        "replay_hash": self.replay_hash,
    }

FederatedTransportDeploymentPreflightManifest dataclass

FederatedTransportDeploymentPreflightManifest(
    schema_name: str,
    schema_version: str,
    batch_id: str,
    preflight_id: str,
    transport: str,
    transport_endpoint: str,
    transport_audit_record: tuple[tuple[str, object], ...],
    transport_audit_hash: str,
    replay_ledger_hash: str,
    transport_execution_permitted: bool,
    raw_data_export_permitted: bool,
    operator_review_required: bool,
    non_actuating: bool,
    preflight_signature: str,
    preflight_hash: str,
)

Deterministic, review-only preflight manifest for transport deployment.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return JSON-safe preflight audit evidence.

Returns

dict[str, object] Return JSON-safe preflight audit evidence.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def to_audit_record(self) -> dict[str, object]:
    """Return JSON-safe preflight audit evidence.

    Returns
    -------
    dict[str, object]
        Return JSON-safe preflight audit evidence.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "batch_id": self.batch_id,
        "preflight_id": self.preflight_id,
        "transport": self.transport,
        "transport_endpoint": self.transport_endpoint,
        "transport_audit_record": [
            [key, value] for key, value in self.transport_audit_record
        ],
        "transport_audit_hash": self.transport_audit_hash,
        "replay_ledger_hash": self.replay_ledger_hash,
        "transport_execution_permitted": self.transport_execution_permitted,
        "raw_data_export_permitted": self.raw_data_export_permitted,
        "operator_review_required": self.operator_review_required,
        "non_actuating": self.non_actuating,
        "preflight_signature": self.preflight_signature,
        "preflight_hash": self.preflight_hash,
    }

Functions:

build_signed_transport_envelopes

build_signed_transport_envelopes(
    node_update_audit_records: Sequence[
        Mapping[str, object]
    ],
    *,
    schema_name: str = _DEFAULT_SCHEMA_NAME,
    schema_version: str = _DEFAULT_SCHEMA_VERSION,
    batch_id: str | None = None,
) -> tuple[FederatedTransportEnvelope, ...]

Build deterministic hash-linked envelopes from node audit records.

Parameters

node_update_audit_records : Sequence[Mapping[str, object]] Federated node update audit records. schema_name : str Transport schema name. schema_version : str Transport schema version. batch_id : str | None Identifier of the transport batch, or None.

Returns

tuple[FederatedTransportEnvelope, ...] The hash-linked signed transport envelopes.

Raises

ValueError If the node audit records are malformed.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def build_signed_transport_envelopes(
    node_update_audit_records: Sequence[Mapping[str, object]],
    *,
    schema_name: str = _DEFAULT_SCHEMA_NAME,
    schema_version: str = _DEFAULT_SCHEMA_VERSION,
    batch_id: str | None = None,
) -> tuple[FederatedTransportEnvelope, ...]:
    """Build deterministic hash-linked envelopes from node audit records.

    Parameters
    ----------
    node_update_audit_records : Sequence[Mapping[str, object]]
        Federated node update audit records.
    schema_name : str
        Transport schema name.
    schema_version : str
        Transport schema version.
    batch_id : str | None
        Identifier of the transport batch, or ``None``.

    Returns
    -------
    tuple[FederatedTransportEnvelope, ...]
        The hash-linked signed transport envelopes.

    Raises
    ------
    ValueError
        If the node audit records are malformed.
    """
    if not isinstance(node_update_audit_records, Sequence) or isinstance(
        node_update_audit_records, (str, bytes, bytearray)
    ):
        raise ValueError("node_update_audit_records must be a sequence of mappings")
    if not node_update_audit_records:
        raise ValueError("node_update_audit_records must be non-empty")
    if schema_name not in _ALLOWED_SCHEMA_NAMES:
        raise ValueError(
            f"schema_name must be one of {_sorted_repr(_ALLOWED_SCHEMA_NAMES)}"
        )
    if schema_version not in _ALLOWED_SCHEMA_VERSIONS:
        raise ValueError(
            f"schema_version must be one of {_sorted_repr(_ALLOWED_SCHEMA_VERSIONS)}"
        )

    validated_records = tuple(
        _normalise_update_record(record) for record in node_update_audit_records
    )
    generated_batch_id = batch_id or _stable_hash(
        {
            "schema_name": schema_name,
            "schema_version": schema_version,
            "records": [record["update_hash"] for record in validated_records],
        }
    )
    node_sequence_counters: dict[str, int] = {}
    node_parent_hashes: dict[str, str] = {}
    built_envelopes: list[FederatedTransportEnvelope] = []
    seen_envelope_ids: set[str] = set()

    for position, record in enumerate(validated_records, start=1):
        node_id = _text(record["node_id"], "node_id")
        node_sequence = node_sequence_counters.get(node_id, 0) + 1
        node_sequence_counters[node_id] = node_sequence

        parent_hash = node_parent_hashes.get(node_id, _ZERO_SHA256)
        if not _is_sha256(parent_hash):
            raise ValueError(f"invalid parent hash for node '{node_id}'")

        envelope_id = _stable_hash(
            {
                "schema_name": schema_name,
                "schema_version": schema_version,
                "batch_id": generated_batch_id,
                "sequence_position": position,
                "node_id": node_id,
                "node_sequence": node_sequence,
                "parent_hash": parent_hash,
                "node_update_hash": record["update_hash"],
            }
        )
        if envelope_id in seen_envelope_ids:
            raise ValueError(f"duplicate envelope id generated for record {position}")
        seen_envelope_ids.add(envelope_id)

        envelope_signature = _build_envelope_signature(
            schema_name=schema_name,
            schema_version=schema_version,
            batch_id=generated_batch_id,
            sequence_position=position,
            node_id=node_id,
            node_sequence=node_sequence,
            parent_hash=parent_hash,
            node_update_hash=record["update_hash"],
            node_update_record=record["payload"],
        )

        envelope_hash = _stable_hash(
            {
                "envelope_id": envelope_id,
                "envelope_signature": envelope_signature,
            }
        )

        envelope = FederatedTransportEnvelope(
            schema_name=schema_name,
            schema_version=schema_version,
            batch_id=generated_batch_id,
            sequence_position=position,
            node_id=node_id,
            node_sequence=node_sequence,
            envelope_id=envelope_id,
            parent_envelope_hash=parent_hash,
            node_update_audit_record=tuple(record["payload"]),
            node_update_audit_hash=record["update_hash"],
            envelope_signature=envelope_signature,
            envelope_hash=envelope_hash,
            transport_execution_permitted=False,
            raw_data_export_permitted=False,
            operator_review_required=True,
        )
        node_parent_hashes[node_id] = envelope_hash
        built_envelopes.append(envelope)

    return tuple(built_envelopes)

validate_federated_transport_batch

validate_federated_transport_batch(
    envelopes: Sequence[FederatedTransportEnvelope],
) -> tuple[FederatedTransportEnvelope, ...]

Validate deterministic hash-links and ordering for an ordered transport batch.

Parameters

envelopes : Sequence[FederatedTransportEnvelope] The ordered transport envelopes.

Returns

tuple[FederatedTransportEnvelope, ...] The validated transport batch.

Raises

ValueError If the hash-links or ordering are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def validate_federated_transport_batch(
    envelopes: Sequence[FederatedTransportEnvelope],
) -> tuple[FederatedTransportEnvelope, ...]:
    """Validate deterministic hash-links and ordering for an ordered transport batch.

    Parameters
    ----------
    envelopes : Sequence[FederatedTransportEnvelope]
        The ordered transport envelopes.

    Returns
    -------
    tuple[FederatedTransportEnvelope, ...]
        The validated transport batch.

    Raises
    ------
    ValueError
        If the hash-links or ordering are invalid.
    """
    if not isinstance(envelopes, Sequence) or isinstance(
        envelopes, (str, bytes, bytearray)
    ):
        raise ValueError("envelopes must be a sequence of transport envelopes")
    if not envelopes:
        raise ValueError("envelopes must be non-empty")

    first = envelopes[0]
    if first.schema_name not in _ALLOWED_SCHEMA_NAMES:
        raise ValueError(
            f"schema_name must be one of {_sorted_repr(_ALLOWED_SCHEMA_NAMES)}"
        )
    if first.schema_version not in _ALLOWED_SCHEMA_VERSIONS:
        raise ValueError(
            f"schema_version must be one of {_sorted_repr(_ALLOWED_SCHEMA_VERSIONS)}"
        )

    seen_envelope_ids: set[str] = set()
    node_last_sequence: dict[str, int] = {}
    node_last_hash: dict[str, str] = {}
    for expected_position, envelope in enumerate(envelopes, start=1):
        if not isinstance(envelope, FederatedTransportEnvelope):
            raise ValueError("envelopes must be FederatedTransportEnvelope instances")

        if envelope.schema_name != first.schema_name:
            raise ValueError("mixed schema_name in transport batch")
        if envelope.schema_version != first.schema_version:
            raise ValueError("mixed schema_version in transport batch")
        if envelope.batch_id != first.batch_id:
            raise ValueError("mixed batch_id in transport batch")
        if envelope.sequence_position != expected_position:
            raise ValueError(
                "envelopes must preserve contiguous sequence_position ordering"
            )
        if envelope.node_id == "":
            raise ValueError("node_id must be non-empty")
        if not _is_sha256(envelope.node_update_audit_hash):
            raise ValueError(
                f"invalid node_update_audit_hash for envelope {envelope.envelope_id}"
            )
        if not _is_sha256(envelope.envelope_hash):
            raise ValueError(
                f"invalid envelope_hash for envelope {envelope.envelope_id}"
            )
        if not _is_sha256(envelope.envelope_signature):
            raise ValueError(
                f"invalid envelope_signature for envelope {envelope.envelope_id}"
            )
        if envelope.envelope_id in seen_envelope_ids:
            raise ValueError(f"duplicate envelope id '{envelope.envelope_id}'")
        seen_envelope_ids.add(envelope.envelope_id)

        if envelope.transport_execution_permitted is not False:
            raise ValueError("transport_execution_permitted must be False")
        if envelope.raw_data_export_permitted is not False:
            raise ValueError("raw_data_export_permitted must be False")
        if envelope.operator_review_required is not True:
            raise ValueError("operator_review_required must be True")

        normalised_node_update = _normalise_update_record(
            dict(envelope.node_update_audit_record)
        )
        if normalised_node_update["update_hash"] != envelope.node_update_audit_hash:
            raise ValueError(
                f"node update hash mismatch for envelope '{envelope.envelope_id}'"
            )

        expected_parent = node_last_hash.get(envelope.node_id, _ZERO_SHA256)
        if envelope.parent_envelope_hash != expected_parent:
            raise ValueError(
                f"parent hash mismatch for node '{envelope.node_id}' at "
                f"position {envelope.sequence_position}"
            )

        expected_node_sequence = node_last_sequence.get(envelope.node_id, 0) + 1
        if envelope.node_sequence != expected_node_sequence:
            raise ValueError(
                f"non-monotonic node_sequence for node '{envelope.node_id}': "
                f"{envelope.node_sequence}"
            )

        expected_signature = _build_envelope_signature(
            schema_name=envelope.schema_name,
            schema_version=envelope.schema_version,
            batch_id=envelope.batch_id,
            sequence_position=envelope.sequence_position,
            node_id=envelope.node_id,
            node_sequence=envelope.node_sequence,
            parent_hash=envelope.parent_envelope_hash,
            node_update_hash=envelope.node_update_audit_hash,
            node_update_record=tuple(envelope.node_update_audit_record),
        )
        expected_hash = _stable_hash(
            {
                "envelope_id": envelope.envelope_id,
                "envelope_signature": expected_signature,
            }
        )
        if envelope.envelope_signature != expected_signature:
            raise ValueError(
                f"signature mismatch for envelope '{envelope.envelope_id}'"
            )
        if envelope.envelope_hash != expected_hash:
            raise ValueError(f"hash mismatch for envelope '{envelope.envelope_id}'")

        node_last_sequence[envelope.node_id] = envelope.node_sequence
        node_last_hash[envelope.node_id] = envelope.envelope_hash

    return tuple(envelopes)

replay_federated_transport_batch

replay_federated_transport_batch(
    envelopes: Sequence[FederatedTransportEnvelope],
) -> FederatedTransportReplayLedger

Replay and materialise a deterministic digest for an ordered transport batch.

Parameters

envelopes : Sequence[FederatedTransportEnvelope] The ordered transport envelopes.

Returns

FederatedTransportReplayLedger The replay ledger for the transport batch.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def replay_federated_transport_batch(
    envelopes: Sequence[FederatedTransportEnvelope],
) -> FederatedTransportReplayLedger:
    """Replay and materialise a deterministic digest for an ordered transport batch.

    Parameters
    ----------
    envelopes : Sequence[FederatedTransportEnvelope]
        The ordered transport envelopes.

    Returns
    -------
    FederatedTransportReplayLedger
        The replay ledger for the transport batch.
    """
    validated = validate_federated_transport_batch(envelopes)
    first = validated[0]
    replay_hash = _stable_hash(
        {
            "schema_name": first.schema_name,
            "schema_version": first.schema_version,
            "batch_id": first.batch_id,
            "envelopes": [envelope.to_audit_record() for envelope in validated],
        }
    )
    node_last = tuple(
        sorted(_group_last_sequence(validated).items(), key=lambda item: item[0])
    )
    return FederatedTransportReplayLedger(
        schema_name=first.schema_name,
        schema_version=first.schema_version,
        batch_id=first.batch_id,
        envelope_count=len(validated),
        envelope_ids=tuple(envelope.envelope_id for envelope in validated),
        node_last_sequences=node_last,
        replay_hash=replay_hash,
    )

build_transport_deployment_preflight_manifest

build_transport_deployment_preflight_manifest(
    transport_declaration: Mapping[str, object],
    *,
    replay_ledger: FederatedTransportReplayLedger,
    schema_name: str = _DEFAULT_SCHEMA_NAME,
    schema_version: str = _DEFAULT_SCHEMA_VERSION,
    batch_id: str | None = None,
) -> FederatedTransportDeploymentPreflightManifest

Build deterministic transport preflight evidence.

Parameters

transport_declaration : Mapping[str, object] The transport declaration to preflight. replay_ledger : FederatedTransportReplayLedger The replay ledger for the transport batch. schema_name : str Transport schema name. schema_version : str Transport schema version. batch_id : str | None Identifier of the transport batch, or None.

Returns

FederatedTransportDeploymentPreflightManifest The transport deployment preflight manifest.

Raises

ValueError If the transport declaration or ledger is invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def build_transport_deployment_preflight_manifest(
    transport_declaration: Mapping[str, object],
    *,
    replay_ledger: FederatedTransportReplayLedger,
    schema_name: str = _DEFAULT_SCHEMA_NAME,
    schema_version: str = _DEFAULT_SCHEMA_VERSION,
    batch_id: str | None = None,
) -> FederatedTransportDeploymentPreflightManifest:
    """Build deterministic transport preflight evidence.

    Parameters
    ----------
    transport_declaration : Mapping[str, object]
        The transport declaration to preflight.
    replay_ledger : FederatedTransportReplayLedger
        The replay ledger for the transport batch.
    schema_name : str
        Transport schema name.
    schema_version : str
        Transport schema version.
    batch_id : str | None
        Identifier of the transport batch, or ``None``.

    Returns
    -------
    FederatedTransportDeploymentPreflightManifest
        The transport deployment preflight manifest.

    Raises
    ------
    ValueError
        If the transport declaration or ledger is invalid.
    """
    if schema_name not in _ALLOWED_SCHEMA_NAMES:
        raise ValueError(
            f"schema_name must be one of {_sorted_repr(_ALLOWED_SCHEMA_NAMES)}"
        )
    if schema_version not in _ALLOWED_SCHEMA_VERSIONS:
        raise ValueError(
            f"schema_version must be one of {_sorted_repr(_ALLOWED_SCHEMA_VERSIONS)}"
        )
    if not isinstance(replay_ledger, FederatedTransportReplayLedger):
        raise ValueError("replay_ledger must be a FederatedTransportReplayLedger")
    if replay_ledger.schema_name != schema_name:
        raise ValueError("replay_ledger.schema_name must match schema_name")
    if replay_ledger.schema_version != schema_version:
        raise ValueError("replay_ledger.schema_version must match schema_version")
    if replay_ledger.batch_id == "":
        raise ValueError("replay_ledger.batch_id must be non-empty")
    if not _is_sha256(replay_ledger.replay_hash):
        raise ValueError("replay_ledger.replay_hash must be a SHA-256 digest")

    normalised_declaration = _normalise_transport_declaration(
        _normalise_transport_input(transport_declaration)
    )

    resolved_batch_id = _text(batch_id or replay_ledger.batch_id, "batch_id")
    if resolved_batch_id != replay_ledger.batch_id:
        raise ValueError("batch_id must match replay_ledger.batch_id")

    transport_audit_record = tuple(
        (key, value)
        for key, value in (
            ("transport", normalised_declaration["transport"]),
            ("endpoint", normalised_declaration["endpoint"]),
            ("owner", normalised_declaration["owner"]),
            ("auth_policy", normalised_declaration["auth_policy"]),
            ("secure_channel", normalised_declaration["secure_channel"]),
            ("replay_supported", normalised_declaration["replay_supported"]),
            ("operator_approved", normalised_declaration["operator_approved"]),
            ("local_path_evidence", normalised_declaration["local_path_evidence"]),
        )
    )
    transport_audit_hash = _stable_hash(
        {
            "transport_audit_record": [list(pair) for pair in transport_audit_record],
            "replay_ledger_hash": replay_ledger.replay_hash,
        }
    )
    preflight_signature = _build_transport_preflight_signature(
        schema_name=schema_name,
        schema_version=schema_version,
        batch_id=resolved_batch_id,
        transport=normalised_declaration["transport"],
        transport_endpoint=normalised_declaration["endpoint"],
        transport_audit_record=transport_audit_record,
        replay_ledger_hash=replay_ledger.replay_hash,
        transport_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
    )
    preflight_id = _stable_hash(
        {
            "schema_name": schema_name,
            "schema_version": schema_version,
            "batch_id": resolved_batch_id,
            "transport_audit_hash": transport_audit_hash,
            "replay_ledger_hash": replay_ledger.replay_hash,
            "transport_endpoint": normalised_declaration["endpoint"],
        }
    )
    preflight_hash = _stable_hash(
        {
            "preflight_id": preflight_id,
            "preflight_signature": preflight_signature,
            "transport_audit_hash": transport_audit_hash,
        }
    )

    manifest = FederatedTransportDeploymentPreflightManifest(
        schema_name=schema_name,
        schema_version=schema_version,
        batch_id=resolved_batch_id,
        preflight_id=preflight_id,
        transport=normalised_declaration["transport"],
        transport_endpoint=normalised_declaration["endpoint"],
        transport_audit_record=transport_audit_record,
        transport_audit_hash=transport_audit_hash,
        replay_ledger_hash=replay_ledger.replay_hash,
        transport_execution_permitted=False,
        raw_data_export_permitted=False,
        operator_review_required=True,
        non_actuating=True,
        preflight_signature=preflight_signature,
        preflight_hash=preflight_hash,
    )
    return validate_transport_deployment_preflight_manifest(manifest)

validate_transport_deployment_preflight_manifest

validate_transport_deployment_preflight_manifest(
    manifest: FederatedTransportDeploymentPreflightManifest,
) -> FederatedTransportDeploymentPreflightManifest

Validate deterministic transport preflight manifest content and hashes.

Parameters

manifest : FederatedTransportDeploymentPreflightManifest The manifest to validate.

Returns

FederatedTransportDeploymentPreflightManifest The validated transport preflight manifest.

Raises

ValueError If the manifest content or hashes are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/federated_transport.py
def validate_transport_deployment_preflight_manifest(
    manifest: FederatedTransportDeploymentPreflightManifest,
) -> FederatedTransportDeploymentPreflightManifest:
    """Validate deterministic transport preflight manifest content and hashes.

    Parameters
    ----------
    manifest : FederatedTransportDeploymentPreflightManifest
        The manifest to validate.

    Returns
    -------
    FederatedTransportDeploymentPreflightManifest
        The validated transport preflight manifest.

    Raises
    ------
    ValueError
        If the manifest content or hashes are invalid.
    """
    if not isinstance(manifest, FederatedTransportDeploymentPreflightManifest):
        raise ValueError(
            "manifest must be a FederatedTransportDeploymentPreflightManifest"
        )

    if manifest.schema_name not in _ALLOWED_SCHEMA_NAMES:
        raise ValueError(
            f"schema_name must be one of {_sorted_repr(_ALLOWED_SCHEMA_NAMES)}"
        )
    if manifest.schema_version not in _ALLOWED_SCHEMA_VERSIONS:
        raise ValueError(
            f"schema_version must be one of {_sorted_repr(_ALLOWED_SCHEMA_VERSIONS)}"
        )
    if manifest.batch_id == "":
        raise ValueError("batch_id must be non-empty")
    if not _is_sha256(manifest.replay_ledger_hash):
        raise ValueError("replay_ledger_hash must be a SHA-256 digest")
    if not _is_sha256(manifest.transport_audit_hash):
        raise ValueError("transport_audit_hash must be a SHA-256 digest")
    if not _is_sha256(manifest.preflight_signature):
        raise ValueError("preflight_signature must be a SHA-256 digest")
    if not _is_sha256(manifest.preflight_hash):
        raise ValueError("preflight_hash must be a SHA-256 digest")

    if manifest.transport_execution_permitted is not False:
        raise ValueError("transport_execution_permitted must be False")
    if manifest.raw_data_export_permitted is not False:
        raise ValueError("raw_data_export_permitted must be False")
    if manifest.operator_review_required is not True:
        raise ValueError("operator_review_required must be True")
    if manifest.non_actuating is not True:
        raise ValueError("non_actuating must be True")

    declaration_from_record = dict(manifest.transport_audit_record)
    if declaration_from_record.get("transport") != manifest.transport:
        raise ValueError("transport mismatch")
    if declaration_from_record.get("endpoint") != manifest.transport_endpoint:
        raise ValueError("transport_endpoint mismatch")
    _normalise_transport_declaration(declaration_from_record)
    expected_signature = _build_transport_preflight_signature(
        schema_name=manifest.schema_name,
        schema_version=manifest.schema_version,
        batch_id=manifest.batch_id,
        transport=manifest.transport,
        transport_endpoint=manifest.transport_endpoint,
        transport_audit_record=manifest.transport_audit_record,
        replay_ledger_hash=manifest.replay_ledger_hash,
        transport_execution_permitted=manifest.transport_execution_permitted,
        raw_data_export_permitted=manifest.raw_data_export_permitted,
        operator_review_required=manifest.operator_review_required,
        non_actuating=manifest.non_actuating,
    )
    if manifest.preflight_signature != expected_signature:
        raise ValueError("preflight_signature mismatch")

    expected_transport_audit_hash = _stable_hash(
        {
            "transport_audit_record": [
                list(pair) for pair in manifest.transport_audit_record
            ],
            "replay_ledger_hash": manifest.replay_ledger_hash,
        }
    )
    if manifest.transport_audit_hash != expected_transport_audit_hash:
        raise ValueError("transport_audit_hash mismatch")

    expected_preflight_id = _stable_hash(
        {
            "schema_name": manifest.schema_name,
            "schema_version": manifest.schema_version,
            "batch_id": manifest.batch_id,
            "transport_audit_hash": manifest.transport_audit_hash,
            "replay_ledger_hash": manifest.replay_ledger_hash,
            "transport_endpoint": manifest.transport_endpoint,
        }
    )
    if manifest.preflight_id != expected_preflight_id:
        raise ValueError("preflight_id mismatch")

    expected_hash = _stable_hash(
        {
            "preflight_id": manifest.preflight_id,
            "preflight_signature": manifest.preflight_signature,
            "transport_audit_hash": manifest.transport_audit_hash,
        }
    )
    if manifest.preflight_hash != expected_hash:
        raise ValueError("preflight_hash mismatch")

    return manifest

Information Geometry and Lineage

Information-geometric control proposals, static scenario examples, and autopoietic lineage inheritance helpers for review-only policy evolution. The information-geometry primitive keeps NumPy as the default audit-stable backend and exposes explicit backend="jax" acceleration with reference-gated parity for Fisher-Rao distance, Wasserstein distance, curvature proxy, and natural-gradient proposals. Both paths remain non-actuating review surfaces. The lineage sandbox generates deterministic child-policy candidates from a parent policy and replay corpus, records accepted/rejected evidence, hashes the lineage and replay corpus, and keeps live merge, hot patching, execution, and actuation disabled. The curated replay corpus spans power-grid recovery, cardiac-rhythm pacing recovery, traffic-flow platooning, and cyber-industrial recontainment so operators can compare policy diffs across domains before any separate inheritance-review workflow. Intergenerational inheritance then signs accepted child-policy records, materialises inherited genomes, records multi-objective replay fitness, and can package deterministic history rows for operator review. The history package links lineage hashes, inheritance hashes, HMAC signature metadata, replay domains, and fitness ranges while keeping direct hot patching and actuation disabled.

information_geometry

Deterministic information-geometry control proposals for geometry-aware control.

Classes

InformationGeometryState dataclass

InformationGeometryState(
    simplex_coordinates: FloatArray,
    target_coordinates: FloatArray,
    metric_tensor: FloatArray,
    tangent_vector: FloatArray,
    curvature_proxy: float,
    geodesic_length: float,
)

Internal geometry state on simplex coordinates.

Attributes are designed to be deterministic and JSON-safe after conversion.

InformationGeometryControlProposal dataclass

InformationGeometryControlProposal(
    action_proposals: tuple[ControlAction, ...],
    fisher_rao_distance: float,
    wasserstein_distance: float,
    natural_gradient_norm: float,
    curvature_proxy: float,
    backend: str,
    claim_boundary: str,
    non_actuating: bool,
    execution_disabled: bool,
    proposal_hash: str,
    state: InformationGeometryState,
)

Review-only control proposal derived from information-geometry metrics.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe audit payload for the proposal.

Returns

dict[str, object] Return a JSON-safe audit payload for the proposal.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe audit payload for the proposal.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe audit payload for the proposal.
    """
    return {
        "action_proposals": [
            {
                "knob": action.knob,
                "scope": action.scope,
                "value": action.value,
                "ttl_s": action.ttl_s,
                "justification": action.justification,
            }
            for action in self.action_proposals
        ],
        "fisher_rao_distance": self.fisher_rao_distance,
        "wasserstein_distance": self.wasserstein_distance,
        "natural_gradient_norm": self.natural_gradient_norm,
        "curvature_proxy": self.curvature_proxy,
        "backend": self.backend,
        "claim_boundary": self.claim_boundary,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "proposal_hash": self.proposal_hash,
        "state": {
            "simplex_coordinates": self.state.simplex_coordinates.tolist(),
            "target_coordinates": self.state.target_coordinates.tolist(),
            "metric_tensor": self.state.metric_tensor.tolist(),
            "tangent_vector": self.state.tangent_vector.tolist(),
            "curvature_proxy": self.state.curvature_proxy,
            "geodesic_length": self.state.geodesic_length,
        },
    }

Functions:

propose_information_geometry_control

propose_information_geometry_control(
    current_distribution: FloatArray
    | list[float]
    | tuple[float, ...],
    target_distribution: FloatArray
    | list[float]
    | tuple[float, ...],
    coupling_gradient: FloatArray
    | list[float]
    | tuple[float, ...]
    | None = None,
    *,
    max_step: float,
    knob: str = _DEFAULT_KNOB,
    scope: str = _DEFAULT_SCOPE,
    backend: str = "numpy",
) -> InformationGeometryControlProposal

Compute a finite, deterministic information-geometry control proposal.

Parameters are validated eagerly and no mutation of caller arrays is performed. The default NumPy backend preserves historical audit hashes; passing backend="jax" uses a JAX-native vectorised metric path and converts the resulting proposal back to JSON-safe NumPy scalars and arrays.

Parameters

current_distribution : FloatArray | list[float] | tuple[float, ...] The current probability distribution. target_distribution : FloatArray | list[float] | tuple[float, ...] The target probability distribution. coupling_gradient : FloatArray | list[float] | tuple[float, ...] | None Gradient of coherence with respect to the coupling, or None. max_step : float Maximum control step magnitude. knob : str Name of the control knob to adjust. scope : str Scope label for the proposed control. backend : str Name of the compute backend to use.

Returns

InformationGeometryControlProposal The deterministic information-geometry control proposal.

Raises

ValueError If the distributions or step are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry.py
def propose_information_geometry_control(
    current_distribution: FloatArray | list[float] | tuple[float, ...],
    target_distribution: FloatArray | list[float] | tuple[float, ...],
    coupling_gradient: FloatArray | list[float] | tuple[float, ...] | None = None,
    *,
    max_step: float,
    knob: str = _DEFAULT_KNOB,
    scope: str = _DEFAULT_SCOPE,
    backend: str = "numpy",
) -> InformationGeometryControlProposal:
    """Compute a finite, deterministic information-geometry control proposal.

    Parameters are validated eagerly and no mutation of caller arrays is performed.
    The default NumPy backend preserves historical audit hashes; passing
    ``backend="jax"`` uses a JAX-native vectorised metric path and converts the
    resulting proposal back to JSON-safe NumPy scalars and arrays.

    Parameters
    ----------
    current_distribution : FloatArray | list[float] | tuple[float, ...]
        The current probability distribution.
    target_distribution : FloatArray | list[float] | tuple[float, ...]
        The target probability distribution.
    coupling_gradient : FloatArray | list[float] | tuple[float, ...] | None
        Gradient of coherence with respect to the coupling, or ``None``.
    max_step : float
        Maximum control step magnitude.
    knob : str
        Name of the control knob to adjust.
    scope : str
        Scope label for the proposed control.
    backend : str
        Name of the compute backend to use.

    Returns
    -------
    InformationGeometryControlProposal
        The deterministic information-geometry control proposal.

    Raises
    ------
    ValueError
        If the distributions or step are invalid.
    """
    simplex = _normalise_simplex(current_distribution, "current_distribution")
    target = _normalise_simplex(target_distribution, "target_distribution")
    if simplex.shape != target.shape:
        raise ValueError("current_distribution and target_distribution must match")

    max_step_value = _as_finite_real(max_step, "max_step", allow_non_positive=False)
    knob = _as_non_empty_str(knob, "knob")
    scope = _as_non_empty_str(scope, "scope")
    backend_name = _normalise_backend_name(backend)

    if coupling_gradient is None:
        objective_gradient = target - simplex
    else:
        objective_gradient = _validate_gradient(
            coupling_gradient,
            simplex.shape,
            "coupling_gradient",
        )

    if backend_name == "jax":
        (
            fisher_rao_distance,
            wasserstein_distance,
            metric_tensor,
            natural_gradient,
            curvature_proxy,
        ) = _compute_information_geometry_jax(
            simplex,
            target,
            objective_gradient,
            max_step_value,
        )
        audit_backend = _JAX_BACKEND
    else:
        fisher_rao_distance = _fisher_rao_distance(simplex, target)
        wasserstein_distance = _wasserstein_distance(simplex, target)
        metric_tensor = _fisher_information_metric(simplex)
        natural_gradient = _natural_gradient_direction(
            objective_gradient, simplex, max_step_value
        )
        curvature_proxy = _curvature_proxy(metric_tensor)
        audit_backend = _BACKEND

    geodesic_length = float(fisher_rao_distance)

    action_value = float(
        np.clip(np.sum(natural_gradient), -max_step_value, max_step_value)
    )
    proposals = (
        ControlAction(
            knob=knob,
            scope=scope,
            value=action_value,
            ttl_s=float(max_step_value),
            justification="information-geometry review proposal",
        ),
    )

    state = InformationGeometryState(
        simplex_coordinates=simplex,
        target_coordinates=target,
        metric_tensor=metric_tensor,
        tangent_vector=natural_gradient,
        curvature_proxy=curvature_proxy,
        geodesic_length=geodesic_length,
    )

    proposal = InformationGeometryControlProposal(
        action_proposals=proposals,
        fisher_rao_distance=fisher_rao_distance,
        wasserstein_distance=wasserstein_distance,
        natural_gradient_norm=float(np.linalg.norm(natural_gradient)),
        curvature_proxy=curvature_proxy,
        backend=audit_backend,
        claim_boundary=_BOUNDARY,
        non_actuating=True,
        execution_disabled=True,
        proposal_hash="",
        state=state,
    )
    proposal_hash = _compute_hash(proposal.to_audit_record())

    return InformationGeometryControlProposal(
        action_proposals=proposals,
        fisher_rao_distance=fisher_rao_distance,
        wasserstein_distance=wasserstein_distance,
        natural_gradient_norm=float(np.linalg.norm(natural_gradient)),
        curvature_proxy=curvature_proxy,
        backend=audit_backend,
        claim_boundary=_BOUNDARY,
        non_actuating=True,
        execution_disabled=True,
        proposal_hash=proposal_hash,
        state=state,
    )

information_geometry_examples

Information geometry control scenarios for non-actuating review.

Classes

DistributionPair dataclass

DistributionPair(
    current_distribution: FloatArray,
    target_distribution: FloatArray,
)

Discrete distribution pair describing review-only geometry transfer targets.

Attributes
current_summary property
current_summary: dict[str, float]

Return summary statistics for the current distribution.

Returns

dict[str, float] Return summary statistics for the current distribution.

target_summary property
target_summary: dict[str, float]

Return summary statistics for the target distribution.

Returns

dict[str, float] Return summary statistics for the target distribution.

Methods:
to_record
to_record() -> dict[str, list[float]]

Return a deterministic JSON-safe record.

Returns

dict[str, list[float]] Return a deterministic JSON-safe record.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry_examples.py
def to_record(self) -> dict[str, list[float]]:
    """Return a deterministic JSON-safe record.

    Returns
    -------
    dict[str, list[float]]
        Return a deterministic JSON-safe record.
    """
    return {
        "current_distribution": self.current_distribution.tolist(),
        "target_distribution": self.target_distribution.tolist(),
    }

InformationGeometryScenario dataclass

InformationGeometryScenario(
    domain: str,
    scenario_id: str,
    distributions: DistributionPair,
    objective_labels: tuple[str, ...],
    control_gradient: tuple[tuple[str, float], ...],
    max_step: float,
    knob_hints: tuple[str, ...] = (),
    non_actuating: bool = True,
    execution_disabled: bool = True,
    claim_boundary: str = InformationGeometryBoundary,
)

Deterministic control scenario for information-geometry review fixtures.

Methods:
scenario_hash
scenario_hash() -> str

Return the deterministic scenario digest.

Returns

str Return the deterministic scenario digest.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry_examples.py
def scenario_hash(self) -> str:
    """Return the deterministic scenario digest.

    Returns
    -------
    str
        Return the deterministic scenario digest.
    """
    return _compute_scenario_hash(
        domain=self.domain,
        scenario_id=self.scenario_id,
        distributions=self.distributions,
        objective_labels=self.objective_labels,
        control_gradient=self.control_gradient,
        knob_hints=self.knob_hints,
        max_step=self.max_step,
        non_actuating=self.non_actuating,
        execution_disabled=self.execution_disabled,
        claim_boundary=self.claim_boundary,
    )
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry_examples.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    distributions = self.distributions.to_record()
    return {
        "domain": self.domain,
        "scenario_id": self.scenario_id,
        "scenario_hash": self.scenario_hash(),
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
        "objective_labels": list(self.objective_labels),
        "max_step": float(self.max_step),
        "control_gradient": [
            [knob, float(value)] for knob, value in self.control_gradient
        ],
        "knob_hints": list(self.knob_hints),
        "current_distribution": distributions["current_distribution"],
        "target_distribution": distributions["target_distribution"],
        "current_distribution_summary": self.distributions.current_summary,
        "target_distribution_summary": self.distributions.target_summary,
    }

Functions:

build_information_geometry_control_scenarios

build_information_geometry_control_scenarios() -> tuple[
    dict[str, object], ...
]

Build deterministic information-geometry control scenarios.

Returns

tuple[dict[str, object], ...] Build deterministic information-geometry control scenarios.

Source code in src/scpn_phase_orchestrator/supervisor/information_geometry_examples.py
def build_information_geometry_control_scenarios() -> tuple[dict[str, object], ...]:
    """Build deterministic information-geometry control scenarios.

    Returns
    -------
    tuple[dict[str, object], ...]
        Build deterministic information-geometry control scenarios.
    """
    records: list[dict[str, object]] = []
    for scenario in _build_static_scenarios():
        _validate_information_geometry_scenario(scenario)
        record = scenario.to_audit_record()
        _validate_scenario_record(record)
        records.append(record)
    return tuple(records)

lineage

Review-only child-policy lineage manifests for replay sandboxes.

Functions:

build_autopoietic_lineage_replay_corpus

build_autopoietic_lineage_replay_corpus() -> tuple[
    dict[str, object], ...
]

Return a deterministic multi-domain replay corpus for lineage review.

The corpus is intentionally offline and compact. It gives the lineage sandbox domain-diverse replay evidence without loading partner data, contacting services, or enabling any live merge path.

Returns

tuple[dict[str, object], ...] Return a deterministic multi-domain replay corpus for lineage review.

Source code in src/scpn_phase_orchestrator/supervisor/lineage.py
def build_autopoietic_lineage_replay_corpus() -> tuple[dict[str, object], ...]:
    """Return a deterministic multi-domain replay corpus for lineage review.

    The corpus is intentionally offline and compact. It gives the lineage
    sandbox domain-diverse replay evidence without loading partner data,
    contacting services, or enabling any live merge path.

    Returns
    -------
    tuple[dict[str, object], ...]
        Return a deterministic multi-domain replay corpus for lineage review.
    """
    return (
        {
            "replay_id": "power_grid_frequency_recovery",
            "domain": "power_grid",
            "scenario": "frequency_recovery_after_load_step",
            "reward": 0.82,
            "safety_margin": 0.24,
            "violations": [],
        },
        {
            "replay_id": "cardiac_rhythm_pacing_recovery",
            "domain": "cardiac_rhythm",
            "scenario": "ventricular_pacing_recovery",
            "reward": 0.78,
            "safety_margin": 0.19,
            "violations": [],
        },
        {
            "replay_id": "traffic_flow_platoon_recovery",
            "domain": "traffic_flow",
            "scenario": "corridor_platoon_recovery",
            "reward": 0.75,
            "safety_margin": 0.16,
            "violations": [],
        },
        {
            "replay_id": "cyber_industrial_recontainment",
            "domain": "cyber_industrial",
            "scenario": "lateral_movement_recontainment",
            "reward": 0.73,
            "safety_margin": 0.14,
            "violations": [],
        },
    )

build_autopoietic_lineage_sandbox

build_autopoietic_lineage_sandbox(
    parent_policy: Mapping[str, object],
    audit_replays: Sequence[Mapping[str, object]],
    *,
    child_budget: int = 3,
    mutation_step: float = 0.02,
    minimum_replay_reward: float = 0.0,
    minimum_safety_margin: float = 0.0,
) -> dict[str, object]

Build a deterministic offline child-policy lineage review manifest.

The sandbox mutates a numeric parent-policy mapping into a bounded set of child candidates, evaluates each candidate only against supplied replay summaries, and emits reviewable policy diffs. It never permits live merge, hot patching, or actuation.

Parameters

parent_policy : Mapping[str, object] The parent policy genome. audit_replays : Sequence[Mapping[str, object]] Audit replay records used to score child candidates. child_budget : int Maximum number of child candidates to evaluate. mutation_step : float Mutation step size applied to the parent genome. minimum_replay_reward : float Minimum replay reward a child must reach to be accepted. minimum_safety_margin : float Minimum safety margin a child must preserve.

Returns

dict[str, object] The offline child-policy lineage review manifest.

Raises

ValueError If the parent policy or replay inputs are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/lineage.py
def build_autopoietic_lineage_sandbox(
    parent_policy: Mapping[str, object],
    audit_replays: Sequence[Mapping[str, object]],
    *,
    child_budget: int = 3,
    mutation_step: float = 0.02,
    minimum_replay_reward: float = 0.0,
    minimum_safety_margin: float = 0.0,
) -> dict[str, object]:
    """Build a deterministic offline child-policy lineage review manifest.

    The sandbox mutates a numeric parent-policy mapping into a bounded set of
    child candidates, evaluates each candidate only against supplied replay
    summaries, and emits reviewable policy diffs. It never permits live merge,
    hot patching, or actuation.

    Parameters
    ----------
    parent_policy : Mapping[str, object]
        The parent policy genome.
    audit_replays : Sequence[Mapping[str, object]]
        Audit replay records used to score child candidates.
    child_budget : int
        Maximum number of child candidates to evaluate.
    mutation_step : float
        Mutation step size applied to the parent genome.
    minimum_replay_reward : float
        Minimum replay reward a child must reach to be accepted.
    minimum_safety_margin : float
        Minimum safety margin a child must preserve.

    Returns
    -------
    dict[str, object]
        The offline child-policy lineage review manifest.

    Raises
    ------
    ValueError
        If the parent policy or replay inputs are invalid.
    """
    if not isinstance(child_budget, int) or isinstance(child_budget, bool):
        raise ValueError("child_budget must be a positive integer")
    if child_budget <= 0:
        raise ValueError("child_budget must be a positive integer")
    step = _finite_non_negative(mutation_step, "mutation_step")
    if step <= 0.0:
        raise ValueError("mutation_step must be positive")
    min_reward = _finite_non_negative(minimum_replay_reward, "minimum_replay_reward")
    min_margin = _finite_non_negative(minimum_safety_margin, "minimum_safety_margin")
    parent = _validated_policy(parent_policy)
    replays = _validated_replays(audit_replays)

    replay_summary = _replay_summary(replays)
    replay_corpus = _replay_corpus_rows(replays)
    replay_domains = tuple(sorted({str(row["domain"]) for row in replay_corpus}))
    child_candidates = [
        _child_candidate(
            parent,
            index=index,
            mutation_step=step,
            replay_summary=replay_summary,
            minimum_replay_reward=min_reward,
            minimum_safety_margin=min_margin,
        )
        for index in range(child_budget)
    ]
    accepted_child_count = sum(
        candidate["status"] == "accepted_for_review" for candidate in child_candidates
    )
    rejected_child_count = child_budget - accepted_child_count
    manifest: dict[str, object] = {
        "schema": "scpn_autopoietic_lineage_sandbox_v1",
        "parent_policy_sha256": _stable_hash(parent),
        "parent_policy_genome": parent,
        "replay_corpus_sha256": _stable_hash(replay_corpus),
        "child_budget": child_budget,
        "child_candidate_count": len(child_candidates),
        "accepted_child_count": accepted_child_count,
        "rejected_child_count": rejected_child_count,
        "minimum_replay_reward": min_reward,
        "minimum_safety_margin": min_margin,
        "mutation_step": step,
        "review_required": True,
        "execution_disabled": True,
        "live_merge_permitted": False,
        "hot_patch_permitted": False,
        "actuation_permitted": False,
        "replay_corpus": replay_corpus,
        "replay_corpus_count": len(replay_corpus),
        "replay_domain_count": len(replay_domains),
        "replay_domains": replay_domains,
        "replay_summary": replay_summary,
        "child_candidates": child_candidates,
    }
    manifest["lineage_sha256"] = _stable_hash(manifest)
    return manifest

build_intergenerational_policy_inheritance

build_intergenerational_policy_inheritance(
    lineage_manifest: Mapping[str, object],
    child_candidate: Mapping[str, object],
    *,
    signer_id: str,
    signing_key: str,
    objective_weights: Mapping[str, object] | None = None,
) -> dict[str, object]

Build signed review metadata for inherited child-policy genomes.

The resulting manifest materialises the inherited policy genome from a reviewed child diff, records replay-fitness components, and signs metadata for operator review. It does not permit direct hot patches or actuation.

Parameters

lineage_manifest : Mapping[str, object] The lineage review manifest. child_candidate : Mapping[str, object] The candidate child policy genome. signer_id : str Identifier of the signing authority. signing_key : str HMAC signing key for the record. objective_weights : Mapping[str, object] | None Per-objective weights, or None for defaults.

Returns

dict[str, object] The signed inherited child-policy review metadata.

Source code in src/scpn_phase_orchestrator/supervisor/lineage.py
def build_intergenerational_policy_inheritance(
    lineage_manifest: Mapping[str, object],
    child_candidate: Mapping[str, object],
    *,
    signer_id: str,
    signing_key: str,
    objective_weights: Mapping[str, object] | None = None,
) -> dict[str, object]:
    """Build signed review metadata for inherited child-policy genomes.

    The resulting manifest materialises the inherited policy genome from a
    reviewed child diff, records replay-fitness components, and signs metadata
    for operator review. It does not permit direct hot patches or actuation.

    Parameters
    ----------
    lineage_manifest : Mapping[str, object]
        The lineage review manifest.
    child_candidate : Mapping[str, object]
        The candidate child policy genome.
    signer_id : str
        Identifier of the signing authority.
    signing_key : str
        HMAC signing key for the record.
    objective_weights : Mapping[str, object] | None
        Per-objective weights, or ``None`` for defaults.

    Returns
    -------
    dict[str, object]
        The signed inherited child-policy review metadata.
    """
    lineage = _validated_lineage_manifest(lineage_manifest)
    child = _validated_review_child(child_candidate)
    signer = _non_empty_string(signer_id, "signer_id")
    key = _non_empty_string(signing_key, "signing_key")
    weights = _validated_objective_weights(objective_weights)
    parent_genome = _parent_genome_from_lineage(lineage)
    inherited_genome = dict(parent_genome)
    for diff in _policy_diff_items(child):
        inherited_genome[str(diff["knob"])] = _finite_number(
            diff["child_value"], "policy_diff.child_value"
        )
    fitness = _multi_objective_fitness(child, weights)
    signed_payload: dict[str, object] = {
        "lineage_sha256": str(lineage["lineage_sha256"]),
        "child_sha256": str(child["child_sha256"]),
        "inherited_policy_genome": inherited_genome,
        "multi_objective_replay_fitness": fitness,
    }
    metadata = {
        "signer_id": signer,
        "signature_algorithm": "hmac-sha256",
        "signature_sha256": _signature(signed_payload, signer=signer, key=key),
    }
    manifest: dict[str, object] = {
        "schema": "scpn_intergenerational_policy_inheritance_v1",
        "lineage_sha256": lineage["lineage_sha256"],
        "parent_policy_sha256": lineage["parent_policy_sha256"],
        "child_sha256": child["child_sha256"],
        "inherited_policy_genome": inherited_genome,
        "policy_diff": child["policy_diff"],
        "multi_objective_replay_fitness": fitness,
        "signed_metadata": metadata,
        "hot_patch_review_required": True,
        "direct_hot_patch_permitted": False,
        "merge_strategy": "reviewed_hot_patch_only",
        "actuation_permitted": False,
    }
    manifest["inheritance_sha256"] = _stable_hash(manifest)
    return manifest

build_intergenerational_policy_inheritance_history

build_intergenerational_policy_inheritance_history(
    lineage_manifest: Mapping[str, object],
    inheritance_manifests: Sequence[Mapping[str, object]],
) -> dict[str, object]

Build deterministic review history for signed inherited-policy records.

The history package joins one lineage sandbox manifest with signed inheritance manifests derived from it. It is evidence for operator review: the package is deterministic, validates disabled direct hot patching and actuation, and does not execute or merge inherited policies.

Parameters

lineage_manifest : Mapping[str, object] The lineage review manifest. inheritance_manifests : Sequence[Mapping[str, object]] Signed inheritance manifests to fold into the history.

Returns

dict[str, object] The review history of signed inherited-policy records.

Raises

ValueError If an inheritance manifest is inconsistent.

Source code in src/scpn_phase_orchestrator/supervisor/lineage.py
def build_intergenerational_policy_inheritance_history(
    lineage_manifest: Mapping[str, object],
    inheritance_manifests: Sequence[Mapping[str, object]],
) -> dict[str, object]:
    """Build deterministic review history for signed inherited-policy records.

    The history package joins one lineage sandbox manifest with signed
    inheritance manifests derived from it. It is evidence for operator review:
    the package is deterministic, validates disabled direct hot patching and
    actuation, and does not execute or merge inherited policies.

    Parameters
    ----------
    lineage_manifest : Mapping[str, object]
        The lineage review manifest.
    inheritance_manifests : Sequence[Mapping[str, object]]
        Signed inheritance manifests to fold into the history.

    Returns
    -------
    dict[str, object]
        The review history of signed inherited-policy records.

    Raises
    ------
    ValueError
        If an inheritance manifest is inconsistent.
    """
    lineage = _validated_lineage_manifest(lineage_manifest)
    if (
        not isinstance(inheritance_manifests, Sequence)
        or isinstance(inheritance_manifests, str | bytes)
        or not inheritance_manifests
    ):
        raise ValueError("inheritance_manifests must be a non-empty sequence")
    records = tuple(
        _validated_inheritance_manifest(manifest, lineage)
        for manifest in inheritance_manifests
    )
    child_rows = tuple(
        _inheritance_history_child_row(index, manifest)
        for index, manifest in enumerate(records)
    )
    lineage_replay_domains = cast(tuple[str, ...], lineage.get("replay_domains", ()))
    replay_domains = tuple(
        str(domain)
        for domain in lineage_replay_domains
        if isinstance(domain, str) and domain
    )
    fitness_scores = tuple(
        float(cast(float, row["fitness_score"])) for row in child_rows
    )
    history: dict[str, object] = {
        "schema": "scpn_intergenerational_policy_inheritance_history_v1",
        "lineage_sha256": str(lineage["lineage_sha256"]),
        "parent_policy_sha256": str(lineage["parent_policy_sha256"]),
        "history_record_count": len(records),
        "signed_metadata_count": sum(
            1 for manifest in records if manifest["signed_metadata"]
        ),
        "replay_domain_count": len(replay_domains),
        "replay_domains": replay_domains,
        "child_rows": child_rows,
        "minimum_fitness_score": min(fitness_scores),
        "maximum_fitness_score": max(fitness_scores),
        "mean_fitness_score": float(np.mean(fitness_scores)),
        "hot_patch_review_required": True,
        "direct_hot_patch_permitted": False,
        "merge_strategy": "reviewed_hot_patch_only",
        "actuation_permitted": False,
        "operator_review_required": True,
    }
    history["history_sha256"] = _stable_hash(history)
    return history

Multiverse and Topos Review

Counterfactual branch simulation, example manifests, branch-risk gates, and categorical policy composition checks. The multiverse simulator keeps NumPy as the default deterministic audit backend and exposes explicit backend="jax" acceleration for larger branch corpora where JAX can place the vectorized rollout on an available accelerator. Reference benchmarks gate JAX output against NumPy branch hashes, topology metrics, order-parameter trajectories, and final phase angles while preserving the non-actuating, execution-disabled review boundary. Multiverse branch rollouts preserve the Kuramoto graph contract by requiring zero diagonal baseline coupling, phase-lag, and topology-mask matrices; matrix branch actions are projected back onto the off-diagonal graph before simulation. Domain scenario fixtures now cover power-grid, cardiac-rhythm, cyber-industrial, traffic-flow, manufacturing process-control, and plasma-control use cases with simulator-compatible K, alpha, zeta, and Psi candidate controls. Studio packages rollout manifests and branch-risk reports through the public scpn_phase_orchestrator.studio.build_multiverse_counterfactual_studio_panel() facade, which preserves the non-actuating claim boundaries, joins branch hashes, renders approval/rejection evidence, and never emits executable actions.

multiverse

Deterministic counterfactual branch rollouts over branch topologies.

The implementation runs vectorised NumPy or optional JAX trajectories for multiple branch interventions in one pass and keeps a strict non-actuation boundary. It is an upstream-safe simulation surface for research, policy gating, and audit review.

Classes

MultiverseBranchSpec dataclass

MultiverseBranchSpec(
    branch_id: str,
    actions: tuple[ControlAction, ...],
    topology_mask: FloatArray | None = None,
)

Declarative counterfactual branch intervention specification.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe branch specification record.

Returns

dict[str, object] Return a JSON-safe branch specification record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe branch specification record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe branch specification record.
    """
    return {
        "branch_id": self.branch_id,
        "actions": [
            {
                "knob": action.knob,
                "scope": action.scope,
                "value": float(action.value),
                "ttl_s": float(action.ttl_s),
                "justification": action.justification,
            }
            for action in self.actions
        ],
        "topology_mask": None
        if self.topology_mask is None
        else self.topology_mask.tolist(),
    }

MultiverseBranchRecord dataclass

MultiverseBranchRecord(
    branch_id: str,
    branch_hash: str,
    action_count: int,
    action_labels: tuple[str, ...],
    topology_edge_count: int,
    topology_scale: float,
    final_R: float,
    mean_R: float,
    min_R: float,
    max_R: float,
    final_psi: float,
)

Audit record for one counterfactual branch rollout.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe branch rollout record.

Returns

dict[str, object] Return a JSON-safe branch rollout record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe branch rollout record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe branch rollout record.
    """
    return {
        "branch_id": self.branch_id,
        "branch_hash": self.branch_hash,
        "action_count": self.action_count,
        "action_labels": list(self.action_labels),
        "topology_edge_count": self.topology_edge_count,
        "topology_scale": self.topology_scale,
        "final_R": self.final_R,
        "mean_R": self.mean_R,
        "min_R": self.min_R,
        "max_R": self.max_R,
        "final_psi": self.final_psi,
    }

MultiverseCounterfactualManifest dataclass

MultiverseCounterfactualManifest(
    schema_name: str,
    schema_version: str,
    branch_records: tuple[MultiverseBranchRecord, ...],
    branch_count: int,
    horizon: int,
    backend: str,
    non_actuating: bool,
    execution_disabled: bool,
    claim_boundary: str,
    manifest_hash: str,
)

Audit manifest for a full multiverse counterfactual rollout.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe multiverse rollout manifest.

Returns

dict[str, object] Return a JSON-safe multiverse rollout manifest.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe multiverse rollout manifest.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe multiverse rollout manifest.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "branch_count": self.branch_count,
        "horizon": self.horizon,
        "backend": self.backend,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
        "branch_records": [
            record.to_audit_record() for record in self.branch_records
        ],
        "manifest_hash": self.manifest_hash,
    }

Functions:

simulate_multiverse_counterfactual_branches

simulate_multiverse_counterfactual_branches(
    phases: NDArray[float64],
    omegas: NDArray[float64],
    baseline_k: NDArray[float64],
    baseline_alpha: NDArray[float64],
    branch_specs: tuple[MultiverseBranchSpec, ...] = (),
    *,
    branch_action_sets: tuple[Sequence[ControlAction], ...]
    | None = None,
    topology_masks: tuple[FloatArray, ...] | None = None,
    baseline_zeta: float = 0.0,
    baseline_psi: float = 0.0,
    horizon: int = 20,
    dt: float = 0.01,
    method: str = "rk4",
    backend: str = "numpy",
) -> MultiverseCounterfactualManifest

Run deterministic branch counterfactual rollouts without actuation.

Parameters

phases : NDArray[np.float64] Oscillator phases in radians, shape (N,). omegas : NDArray[np.float64] Natural frequencies in rad/s, shape (N,). baseline_k : NDArray[np.float64] Baseline coupling matrix K_nm, shape (N, N). baseline_alpha : NDArray[np.float64] Baseline phase-lag matrix, shape (N, N). branch_specs : tuple[MultiverseBranchSpec, ...] Specifications of the counterfactual branches. branch_action_sets : tuple[Sequence[ControlAction], ...] | None Per-branch control-action sequences, or None. topology_masks : tuple[FloatArray, ...] | None Per-branch topology masks, or None. baseline_zeta : float Baseline external drive strength ζ. baseline_psi : float Baseline external drive reference phase Ψ in radians. horizon : int Rollout horizon in steps. dt : float Integration step size. method : str Integration method (euler, rk4, or rk45). backend : str Name of the compute backend to use.

Returns

MultiverseCounterfactualManifest The multiverse counterfactual rollout manifest.

Raises

ValueError If the branch specs or rollout inputs are invalid.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse.py
def simulate_multiverse_counterfactual_branches(
    phases: NDArray[np.float64],
    omegas: NDArray[np.float64],
    baseline_k: NDArray[np.float64],
    baseline_alpha: NDArray[np.float64],
    branch_specs: tuple[MultiverseBranchSpec, ...] = (),
    *,
    branch_action_sets: tuple[Sequence[ControlAction], ...] | None = None,
    topology_masks: tuple[FloatArray, ...] | None = None,
    baseline_zeta: float = 0.0,
    baseline_psi: float = 0.0,
    horizon: int = 20,
    dt: float = 0.01,
    method: str = "rk4",
    backend: str = "numpy",
) -> MultiverseCounterfactualManifest:
    """Run deterministic branch counterfactual rollouts without actuation.

    Parameters
    ----------
    phases : NDArray[np.float64]
        Oscillator phases in radians, shape ``(N,)``.
    omegas : NDArray[np.float64]
        Natural frequencies in rad/s, shape ``(N,)``.
    baseline_k : NDArray[np.float64]
        Baseline coupling matrix ``K_nm``, shape ``(N, N)``.
    baseline_alpha : NDArray[np.float64]
        Baseline phase-lag matrix, shape ``(N, N)``.
    branch_specs : tuple[MultiverseBranchSpec, ...]
        Specifications of the counterfactual branches.
    branch_action_sets : tuple[Sequence[ControlAction], ...] | None
        Per-branch control-action sequences, or ``None``.
    topology_masks : tuple[FloatArray, ...] | None
        Per-branch topology masks, or ``None``.
    baseline_zeta : float
        Baseline external drive strength ``ζ``.
    baseline_psi : float
        Baseline external drive reference phase ``Ψ`` in radians.
    horizon : int
        Rollout horizon in steps.
    dt : float
        Integration step size.
    method : str
        Integration method (``euler``, ``rk4``, or ``rk45``).
    backend : str
        Name of the compute backend to use.

    Returns
    -------
    MultiverseCounterfactualManifest
        The multiverse counterfactual rollout manifest.

    Raises
    ------
    ValueError
        If the branch specs or rollout inputs are invalid.
    """
    phases_arr = _coerce_float_array("phases", phases)
    omegas_arr = _coerce_float_array("omegas", omegas)
    baseline_k_arr = _coerce_float_array("baseline_k", baseline_k)
    baseline_alpha_arr = _coerce_float_array("baseline_alpha", baseline_alpha)

    if phases_arr.ndim != 1 or omegas_arr.ndim != 1:
        raise ValueError("phases and omegas must be 1-D arrays")

    n_osc = len(phases_arr)
    _require_shape("phases", phases_arr, (n_osc,))
    _require_shape("omegas", omegas_arr, (n_osc,))
    _require_shape("baseline_k", baseline_k_arr, (n_osc, n_osc))
    _require_shape("baseline_alpha", baseline_alpha_arr, (n_osc, n_osc))
    _require_zero_diagonal("baseline_k", baseline_k_arr)
    _require_zero_diagonal("baseline_alpha", baseline_alpha_arr)

    horizon_i = _require_positive_int(horizon, "horizon")
    dt_f = _require_positive_real(dt, "dt")
    baseline_zeta_f = _require_finite_real(baseline_zeta, "baseline_zeta")
    baseline_psi_f = _require_finite_real(baseline_psi, "baseline_psi")
    backend_name = _normalise_backend(backend)
    if method not in {"euler", "rk4"}:
        raise ValueError("method must be 'euler' or 'rk4'")

    normalised_specs = _normalise_branch_specs(
        branch_specs=tuple(branch_specs),
        branch_action_sets=(
            tuple(tuple(action_set) for action_set in branch_action_sets)
            if branch_action_sets is not None
            else None
        ),
        topology_masks=(None if topology_masks is None else tuple(topology_masks)),
        n_osc=n_osc,
    )
    branch_count = len(normalised_specs)

    if branch_count < 1:
        raise ValueError("at least one branch is required")

    knm_cube = np.empty((branch_count, n_osc, n_osc), dtype=np.float64)
    alpha_cube = np.empty((branch_count, n_osc, n_osc), dtype=np.float64)
    zeta_vec = np.empty(branch_count, dtype=np.float64)
    psi_vec = np.empty(branch_count, dtype=np.float64)
    action_labels: list[tuple[str, ...]] = []
    topology_edge_count: list[int] = []
    topology_scale: list[float] = []
    hashes: list[str] = []

    for index, spec in enumerate(normalised_specs):
        spec_knm, spec_alpha, spec_zeta, spec_psi, labels, edge_count, topo_scale = (
            _apply_branch_actions(
                branch_id=spec.branch_id,
                baseline_k=baseline_k_arr,
                baseline_alpha=baseline_alpha_arr,
                baseline_zeta=baseline_zeta_f,
                baseline_psi=baseline_psi_f,
                actions=spec.actions,
                topology_mask=spec.topology_mask,
            )
        )
        knm_cube[index] = spec_knm
        alpha_cube[index] = spec_alpha
        zeta_vec[index] = spec_zeta
        psi_vec[index] = spec_psi
        action_labels.append(labels)
        topology_edge_count.append(edge_count)
        topology_scale.append(topo_scale)
        hashes.append(
            _branch_hash(
                spec=spec,
                action_count=len(labels),
                topology_edge_count=edge_count,
                topology_scale=topo_scale,
            )
        )

    if backend_name == "jax":
        R_traj, psi_traj = _rollout_jax(
            phases=phases_arr,
            omegas=omegas_arr,
            knm=knm_cube,
            alpha=alpha_cube,
            zeta=zeta_vec,
            psi=psi_vec,
            horizon=horizon_i,
            dt=dt_f,
            method=method,
        )
        audit_backend = _JAX_BACKEND_NAME
    else:
        R_traj, psi_traj = _rollout_numpy(
            phases=phases_arr,
            omegas=omegas_arr,
            knm=knm_cube,
            alpha=alpha_cube,
            zeta=zeta_vec,
            psi=psi_vec,
            horizon=horizon_i,
            dt=dt_f,
            method=method,
        )
        audit_backend = _NUMPY_BACKEND_NAME

    if not np.all(np.isfinite(R_traj)) or not np.all(np.isfinite(psi_traj)):
        raise ValueError("rollout produced non-finite values")

    records = tuple(
        MultiverseBranchRecord(
            branch_id=spec.branch_id,
            branch_hash=hashes[index],
            action_count=non_negative_int(len(spec.actions), name="action_count"),
            action_labels=action_labels[index],
            topology_edge_count=topology_edge_count[index],
            topology_scale=topology_scale[index],
            final_R=float(R_traj[-1, index]),
            mean_R=float(np.mean(R_traj[:, index])),
            min_R=float(np.min(R_traj[:, index])),
            max_R=float(np.max(R_traj[:, index])),
            final_psi=float(psi_traj[-1, index]),
        )
        for index, spec in enumerate(normalised_specs)
    )

    manifest_payload: dict[str, Any] = {
        "schema_name": "multiverse_counterfactual_rollout",
        "schema_version": "0.1.0",
        "branch_count": branch_count,
        "horizon": horizon_i,
        "backend": audit_backend,
        "non_actuating": True,
        "execution_disabled": True,
        "claim_boundary": "counterfactual_branch_rollout_not_live_actuation",
        "branch_records": [record.to_audit_record() for record in records],
        "manifest_hash": "",
    }
    manifest_hash = _stable_hash(manifest_payload)

    return MultiverseCounterfactualManifest(
        schema_name="multiverse_counterfactual_rollout",
        schema_version="0.1.0",
        branch_records=records,
        branch_count=branch_count,
        horizon=horizon_i,
        backend=audit_backend,
        non_actuating=True,
        execution_disabled=True,
        claim_boundary="counterfactual_branch_rollout_not_live_actuation",
        manifest_hash=manifest_hash,
    )

multiverse_examples

Multiverse counterfactual scenario examples for branch review.

Classes

BranchCandidate dataclass

BranchCandidate(
    candidate_id: str,
    knob_variations: tuple[tuple[str, float], ...],
    topology_variations: tuple[str, ...],
    objective_labels: tuple[str, ...],
    non_actuating: bool = True,
    execution_disabled: bool = True,
    claim_boundary: str = CounterfactualBoundary,
)

Single deterministic branch candidate for a counterfactual rollout scenario.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_examples.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "candidate_id": self.candidate_id,
        "knob_variations": [[name, value] for name, value in self.knob_variations],
        "topology_variations": list(self.topology_variations),
        "objective_labels": list(self.objective_labels),
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
    }

DomainScenario dataclass

DomainScenario(
    domain: str,
    scenario_id: str,
    initial_phases: NDArray[float64],
    initial_omegas: NDArray[float64],
    branch_candidates: tuple[BranchCandidate, ...],
    objective_labels: tuple[str, ...],
    non_actuating: bool = True,
    execution_disabled: bool = True,
    claim_boundary: str = CounterfactualBoundary,
)

Deterministic scenario definition for aggregate multiverse benchmarks.

Methods:
scenario_hash
scenario_hash() -> str

Return the deterministic scenario digest.

Returns

str Return the deterministic scenario digest.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_examples.py
def scenario_hash(self) -> str:
    """Return the deterministic scenario digest.

    Returns
    -------
    str
        Return the deterministic scenario digest.
    """
    return _compute_scenario_hash(
        domain=self.domain,
        scenario_id=self.scenario_id,
        initial_phases=self.initial_phases,
        initial_omegas=self.initial_omegas,
        branch_candidates=self.branch_candidates,
        objective_labels=self.objective_labels,
    )
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_examples.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "domain": self.domain,
        "scenario_id": self.scenario_id,
        "scenario_hash": self.scenario_hash(),
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "claim_boundary": self.claim_boundary,
        "initial_phases_summary": _summary(self.initial_phases),
        "initial_omegas_summary": _summary(self.initial_omegas),
        "initial_phases": self.initial_phases.tolist(),
        "initial_omegas": self.initial_omegas.tolist(),
        "branch_candidates": [
            candidate.to_audit_record() for candidate in self.branch_candidates
        ],
        "objective_labels": list(self.objective_labels),
    }

Functions:

build_multiverse_domain_scenarios

build_multiverse_domain_scenarios() -> tuple[
    dict[str, object], ...
]

Build deterministic multiverse domain scenario records.

Returns

tuple[dict[str, object], ...] Build deterministic multiverse domain scenario records.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_examples.py
def build_multiverse_domain_scenarios() -> tuple[dict[str, object], ...]:
    """Build deterministic multiverse domain scenario records.

    Returns
    -------
    tuple[dict[str, object], ...]
        Build deterministic multiverse domain scenario records.
    """
    scenarios = _build_static_scenarios()
    records: list[dict[str, object]] = []

    for scenario in scenarios:
        _validate_scenario(scenario)
        record = scenario.to_audit_record()
        _verify_record_hash(scenario, record)
        records.append(record)

    return tuple(records)

multiverse_risk

Fail-closed review gate over precomputed branch rollout manifests.

Classes

MultiverseRiskThresholds dataclass

MultiverseRiskThresholds(
    min_mean_R: float = 0.0,
    min_final_R: float = 0.0,
    max_action_count: int = 64,
    max_topology_edge_count: int | None = None,
    max_topology_scale: float | None = None,
)

Guard thresholds for branch review in the multiverse gate.

BranchRiskDecision dataclass

BranchRiskDecision(
    branch_id: str,
    branch_hash: str,
    final_R: float,
    mean_R: float,
    min_R: float,
    max_R: float,
    action_count: int,
    topology_edge_count: int | None,
    topology_scale: float | None,
    approved: bool,
    rejection_reasons: tuple[str, ...],
)

Outcome for one branch in a manifest.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe branch decision record.

Returns

dict[str, object] Return a JSON-safe branch decision record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_risk.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe branch decision record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe branch decision record.
    """
    return {
        "branch_id": self.branch_id,
        "branch_hash": self.branch_hash,
        "final_R": self.final_R,
        "mean_R": self.mean_R,
        "min_R": self.min_R,
        "max_R": self.max_R,
        "action_count": self.action_count,
        "topology_edge_count": self.topology_edge_count,
        "topology_scale": self.topology_scale,
        "approved": self.approved,
        "rejection_reasons": list(self.rejection_reasons),
    }

MultiverseRiskReport dataclass

MultiverseRiskReport(
    schema_name: str,
    schema_version: str,
    branch_decisions: tuple[BranchRiskDecision, ...],
    approved_count: int,
    rejected_count: int,
    safest_branch_id: str | None,
    safest_branch_hash: str | None,
    rejection_reasons: tuple[str, ...],
    claim_boundary: str,
    non_actuating: bool,
    execution_disabled: bool,
    report_hash: str,
)

JSON-safe aggregate of the branch-risk review decision.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a JSON-safe multiverse risk gate audit record.

Returns

dict[str, object] Return a JSON-safe multiverse risk gate audit record.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_risk.py
def to_audit_record(self) -> dict[str, object]:
    """Return a JSON-safe multiverse risk gate audit record.

    Returns
    -------
    dict[str, object]
        Return a JSON-safe multiverse risk gate audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "branch_count": len(self.branch_decisions),
        "approved_count": self.approved_count,
        "rejected_count": self.rejected_count,
        "safest_branch_id": self.safest_branch_id,
        "safest_branch_hash": self.safest_branch_hash,
        "rejection_reasons": list(self.rejection_reasons),
        "branch_decisions": [
            decision.to_audit_record() for decision in self.branch_decisions
        ],
        "claim_boundary": self.claim_boundary,
        "non_actuating": self.non_actuating,
        "execution_disabled": self.execution_disabled,
        "report_hash": self.report_hash,
    }

Functions:

evaluate_multiverse_branch_risk

evaluate_multiverse_branch_risk(
    manifest: Mapping[str, object],
    thresholds: MultiverseRiskThresholds | None = None,
) -> MultiverseRiskReport

Evaluate branch risk decisions for a branch manifest without actuation.

Parameters

manifest : Mapping[str, object] The branch rollout manifest to evaluate. thresholds : MultiverseRiskThresholds | None Risk thresholds, or None for defaults.

Returns

MultiverseRiskReport The multiverse risk-gate report.

Source code in src/scpn_phase_orchestrator/supervisor/multiverse_risk.py
def evaluate_multiverse_branch_risk(
    manifest: Mapping[str, object],
    thresholds: MultiverseRiskThresholds | None = None,
) -> MultiverseRiskReport:
    """Evaluate branch risk decisions for a branch manifest without actuation.

    Parameters
    ----------
    manifest : Mapping[str, object]
        The branch rollout manifest to evaluate.
    thresholds : MultiverseRiskThresholds | None
        Risk thresholds, or ``None`` for defaults.

    Returns
    -------
    MultiverseRiskReport
        The multiverse risk-gate report.
    """
    thresholds = _normalise_thresholds(thresholds)
    branches = _extract_manifest_branches(manifest)

    branch_decisions = tuple(
        _build_branch_decision(branch=branch, thresholds=thresholds)
        for branch in branches
    )

    approved = tuple(decision for decision in branch_decisions if decision.approved)
    rejected = tuple(decision for decision in branch_decisions if not decision.approved)
    safest_branch_id, safest_branch_hash = _select_safest_branch(branch_decisions)

    report = MultiverseRiskReport(
        schema_name="multiverse_branch_risk_gate",
        schema_version="0.1.0",
        branch_decisions=branch_decisions,
        approved_count=len(approved),
        rejected_count=len(rejected),
        safest_branch_id=safest_branch_id,
        safest_branch_hash=safest_branch_hash,
        rejection_reasons=tuple(
            sorted(
                {
                    reason
                    for decision in rejected
                    for reason in decision.rejection_reasons
                }
            )
        ),
        claim_boundary="counterfactual_branch_risk_gate_not_live_actuation",
        non_actuating=True,
        execution_disabled=True,
        report_hash="",
    )

    record = report.to_audit_record()
    report_hash = _build_report_hash(record)
    return MultiverseRiskReport(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        branch_decisions=report.branch_decisions,
        approved_count=report.approved_count,
        rejected_count=report.rejected_count,
        safest_branch_id=report.safest_branch_id,
        safest_branch_hash=report.safest_branch_hash,
        rejection_reasons=report.rejection_reasons,
        claim_boundary=report.claim_boundary,
        non_actuating=report.non_actuating,
        execution_disabled=report.execution_disabled,
        report_hash=report_hash,
    )

topos_policy

Deterministic audit/proof-obligation validation for policy composition.

Classes

PolicyCompositionObject dataclass

PolicyCompositionObject(
    name: str,
    regimes: tuple[str, ...],
    action_labels: tuple[str, ...],
)

Stable policy composition object derived from a PolicyRule.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/topos_policy.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "name": self.name,
        "regimes": list(self.regimes),
        "action_labels": list(self.action_labels),
    }

PolicyCompositionMorphism dataclass

PolicyCompositionMorphism(
    source: str,
    target: str,
    label: str,
    deterministic: bool = True,
)

Deterministic relation between composition objects and labelled action slots.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/topos_policy.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "source": self.source,
        "target": self.target,
        "label": self.label,
        "deterministic": self.deterministic,
    }

PolicyCompositionObligation dataclass

PolicyCompositionObligation(
    name: str, status: str, evidence: str
)

Review-only proof obligation outcome.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/topos_policy.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "name": self.name,
        "status": self.status,
        "evidence": self.evidence,
    }

PolicyCompositionValidationReport dataclass

PolicyCompositionValidationReport(
    schema_name: str,
    schema_version: str,
    object_count: int,
    morphism_count: int,
    obligation_records: tuple[
        PolicyCompositionObligation, ...
    ],
    objects: tuple[PolicyCompositionObject, ...],
    morphisms: tuple[PolicyCompositionMorphism, ...],
    passed: bool,
    report_hash: str,
    proof_boundary: str,
    non_actuating: bool = True,
)

JSON-safe deterministic validation report for policy composition.

Methods:
to_audit_record
to_audit_record() -> dict[str, object]

Return a deterministic JSON-safe audit record.

Returns

dict[str, object] Return a deterministic JSON-safe audit record.

Source code in src/scpn_phase_orchestrator/supervisor/topos_policy.py
def to_audit_record(self) -> dict[str, object]:
    """Return a deterministic JSON-safe audit record.

    Returns
    -------
    dict[str, object]
        Return a deterministic JSON-safe audit record.
    """
    return {
        "schema_name": self.schema_name,
        "schema_version": self.schema_version,
        "object_count": self.object_count,
        "morphism_count": self.morphism_count,
        "obligation_records": [
            obligation.to_audit_record() for obligation in self.obligation_records
        ],
        "objects": [obj.to_audit_record() for obj in self.objects],
        "morphisms": [morphism.to_audit_record() for morphism in self.morphisms],
        "passed": self.passed,
        "report_hash": self.report_hash,
        "proof_boundary": self.proof_boundary,
        "non_actuating": self.non_actuating,
    }

Functions:

validate_policy_composition_category

validate_policy_composition_category(
    rules: tuple[PolicyRule, ...] | list[PolicyRule],
) -> PolicyCompositionValidationReport

Validate PolicyRule collections as a categorical composition proof boundary.

Parameters

rules : tuple[PolicyRule, ...] | list[PolicyRule] The policy rules to export or validate.

Returns

PolicyCompositionValidationReport The categorical composition validation report.

Raises

ValueError If the rules violate the composition proof boundary.

Source code in src/scpn_phase_orchestrator/supervisor/topos_policy.py
def validate_policy_composition_category(
    rules: tuple[PolicyRule, ...] | list[PolicyRule],
) -> PolicyCompositionValidationReport:
    """Validate PolicyRule collections as a categorical composition proof boundary.

    Parameters
    ----------
    rules : tuple[PolicyRule, ...] | list[PolicyRule]
        The policy rules to export or validate.

    Returns
    -------
    PolicyCompositionValidationReport
        The categorical composition validation report.

    Raises
    ------
    ValueError
        If the rules violate the composition proof boundary.
    """
    if isinstance(rules, tuple | list):
        rule_list = list(rules)
    else:
        raise ValueError("rules must be a tuple or list of PolicyRule objects")

    if not rule_list:
        raise ValueError("rules collection must be non-empty")

    if not all(isinstance(rule, PolicyRule) for rule in rule_list):
        raise ValueError("rules must contain only PolicyRule objects")

    canonical_rule_names: dict[int, str] = {}
    obligations: list[PolicyCompositionObligation] = []
    _add_obligation(
        obligations,
        name="rules_collection_valid",
        passed=True,
        evidence="rules collection is a non-empty tuple/list of PolicyRule",
    )

    name_failures: set[str] = set()
    raw_names = [rule.name for rule in rule_list]
    normalized_names: list[str] = []
    for rule, raw_name in zip(rule_list, raw_names, strict=True):
        if not isinstance(raw_name, str):
            raise ValueError("rule names must be strings")
        normalized_name = raw_name.strip()
        if not normalized_name:
            raise ValueError("rule names must be non-empty")
        canonical_rule_names[id(rule)] = normalized_name
        normalized_names.append(normalized_name)

    duplicates = {
        name for name in set(normalized_names) if normalized_names.count(name) > 1
    }
    if duplicates:
        name_failures.update(duplicates)
        _add_obligation(
            obligations,
            name="rule_names_unique",
            passed=False,
            evidence="rule names must be unique and stable",
        )
    else:
        _add_obligation(
            obligations,
            name="rule_names_unique",
            passed=True,
            evidence="rule names are unique",
        )

    objects: list[PolicyCompositionObject] = []
    morphisms: list[PolicyCompositionMorphism] = []
    morphism_labels: set[str] = set()
    all_ok = True

    for rule in sorted(rule_list, key=lambda item: canonical_rule_names[id(item)]):
        rule_name = canonical_rule_names[id(rule)]
        rule_ok = True
        if rule_name in name_failures:
            rule_ok = False

        regimes_ok, regime_values, regime_evidence = _is_non_empty_str_list(
            rule.regimes
        )
        if not regimes_ok:
            rule_ok = False
            _add_obligation(
                obligations,
                name=f"rule.{rule_name}.regimes",
                passed=False,
                evidence=f"invalid regimes: {regime_evidence}",
            )
        else:
            _add_obligation(
                obligations,
                name=f"rule.{rule_name}.regimes",
                passed=True,
                evidence="deterministic and non-empty regimes list",
            )

        try:
            cond_logic = None
            if isinstance(rule.condition, CompoundCondition):
                cond_logic = _normalised_logic(rule.condition.logic)
                cond_conditions = rule.condition.conditions
                if not isinstance(cond_conditions, list) and not isinstance(
                    cond_conditions, tuple
                ):
                    raise ValueError(
                        "compound condition entries must be a list or tuple"
                    )
                if not cond_conditions:
                    raise ValueError("compound condition must contain conditions")
                if len(cond_conditions) > _MAX_COMPOUND_CONDITIONS:
                    raise ValueError("compound condition is unbounded")
                if any(
                    not isinstance(cond, PolicyCondition) for cond in cond_conditions
                ):
                    raise ValueError(
                        "compound condition members must be PolicyCondition"
                    )
                for cond in cond_conditions:
                    _validate_policy_condition(cond)
                _add_obligation(
                    obligations,
                    name=f"rule.{rule_name}.condition",
                    passed=True,
                    evidence=f"compound condition with {cond_logic}",
                )
            elif isinstance(rule.condition, PolicyCondition):
                cond_logic = "ATOMIC"
                _validate_policy_condition(rule.condition)
                _add_obligation(
                    obligations,
                    name=f"rule.{rule_name}.condition",
                    passed=True,
                    evidence="atomic policy condition",
                )
            else:
                raise ValueError(
                    "condition must be PolicyCondition or CompoundCondition"
                )
        except ValueError as error:
            rule_ok = False
            _add_obligation(
                obligations,
                name=f"rule.{rule_name}.condition",
                passed=False,
                evidence=str(error),
            )
            cond_logic = None

        actions = rule.actions
        action_labels: list[str] = []
        if not isinstance(actions, list):
            rule_ok = False
            _add_obligation(
                obligations,
                name=f"rule.{rule_name}.actions",
                passed=False,
                evidence="actions must be a list",
            )
        else:
            try:
                if not actions:
                    raise ValueError("at least one action is required")
                for action in actions:
                    action_labels.append(_as_action_label(action))
                _add_obligation(
                    obligations,
                    name=f"rule.{rule_name}.actions",
                    passed=True,
                    evidence=f"{len(action_labels)} deterministic action label(s)",
                )
            except ValueError as error:
                rule_ok = False
                _add_obligation(
                    obligations,
                    name=f"rule.{rule_name}.actions",
                    passed=False,
                    evidence=str(error),
                )

        if rule_ok and cond_logic is not None and action_labels and regimes_ok:
            norm_regimes = tuple(sorted(set(regime_values)))
            obj = PolicyCompositionObject(
                name=rule_name,
                regimes=norm_regimes,
                action_labels=tuple(sorted(action_labels)),
            )
            objects.append(obj)

            for regime in obj.regimes:
                for action_label in obj.action_labels:
                    morphism_label = f"{obj.name}|{regime}|{action_label}"
                    if morphism_label in morphism_labels:
                        continue
                    morphism_labels.add(morphism_label)
                    morphisms.append(
                        PolicyCompositionMorphism(
                            source=obj.name,
                            target=obj.name,
                            label=morphism_label,
                        )
                    )

        all_ok = all_ok and rule_ok

    obligations = sorted(obligations, key=lambda item: item.name)
    objects = sorted(objects, key=lambda item: item.name)
    morphisms = sorted(
        morphisms,
        key=lambda item: (item.source, item.target, item.label),
    )
    report = PolicyCompositionValidationReport(
        schema_name=_SCHEMA_NAME,
        schema_version=_SCHEMA_VERSION,
        object_count=len(objects),
        morphism_count=len(morphisms),
        obligation_records=tuple(obligations),
        objects=tuple(objects),
        morphisms=tuple(morphisms),
        passed=all_ok and "failed" not in [ob.status for ob in obligations],
        report_hash="",
        proof_boundary=_PROOF_BOUNDARY,
        non_actuating=True,
    )
    return PolicyCompositionValidationReport(
        schema_name=report.schema_name,
        schema_version=report.schema_version,
        object_count=report.object_count,
        morphism_count=report.morphism_count,
        obligation_records=report.obligation_records,
        objects=report.objects,
        morphisms=report.morphisms,
        passed=report.passed,
        report_hash=_build_report_hash(report.to_audit_record()),
        proof_boundary=report.proof_boundary,
        non_actuating=report.non_actuating,
    )