Source code for ralph.policy.models._policy_bundle
"""PolicyBundle Pydantic model."""
from typing import Self
from pydantic import Field, model_validator
from ralph.policy.models._agents_policy import AgentsPolicy
from ralph.policy.models._artifacts_policy import ArtifactsPolicy
from ralph.policy.models._frozen_policy_model import _FrozenPolicyModel
from ralph.policy.models._pipeline_policy import PipelinePolicy
[docs]
class PolicyBundle(_FrozenPolicyModel):
"""Aggregate of all three policy documents."""
agents: AgentsPolicy = Field(..., description="Agent chains and drain bindings")
pipeline: PipelinePolicy = Field(..., description="Phase graph and routing")
artifacts: ArtifactsPolicy = Field(..., description="Artifact contracts per drain")
@model_validator(mode="after")
def all_pipeline_drains_are_bound(self) -> Self:
unbound: list[str] = []
for phase_name, phase_def in self.pipeline.phases.items():
if phase_def.role == "terminal":
continue
if phase_name == self.pipeline.terminal_phase:
continue
if phase_def.drain not in self.agents.agent_drains:
unbound.append(phase_def.drain)
if unbound:
raise ValueError(
f"Pipeline uses unbound drains: {sorted(set(unbound))}. "
f"Each drain must have a binding in agents.agent_drains."
)
return self
@model_validator(mode="after")
def analysis_decision_vocabulary_present(self) -> Self:
analysis_phases = {
name: defn for name, defn in self.pipeline.phases.items() if defn.role == "analysis"
}
for phase_name, phase_def in analysis_phases.items():
matching_artifacts = [
art for art in self.artifacts.artifacts.values() if art.drain == phase_def.drain
]
if not any(a.decision_vocabulary for a in matching_artifacts):
raise ValueError(
f"Phase '{phase_name}' has role='analysis' but no matching "
f"artifact contract has a decision_vocabulary defined"
)
return self