#[cfg(feature = "server")]
use std::borrow::Cow;
use serde::{Deserialize, Deserializer, Serialize};
#[cfg(feature = "server")]
use utoipa::openapi::RefOr;
#[cfg(feature = "server")]
use utoipa::openapi::schema::{ObjectBuilder, OneOfBuilder, Schema, Type};
#[cfg(feature = "server")]
use utoipa::{PartialSchema, ToSchema};
use crate::card::drift::{DriftSpec, DriftValidationError};
use crate::card::eval::EvalSpec;
use crate::card::operator::OperatorSpec;
use crate::card::trigger::TriggerSpec;
use crate::ids::BindingId;
use crate::reference::{CardRef, InlineableRef, Ref};
use crate::vala::eval::{AssertionTask, LlmJudgeTask};
use crate::verification::VerificationError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct VerifierSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub implementation: VerifierImplementation,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(
tag = "kind",
content = "spec",
rename_all = "snake_case",
deny_unknown_fields
)]
#[allow(
clippy::large_enum_variant,
reason = "mirrors the `Spec` contract; boxing a variant would change the authored wire shape's ergonomics for every consumer that matches on it"
)]
pub enum VerifierImplementation {
Drift(DriftSpec),
Eval(EvalSpec),
Task(TaskVerifierSpec),
}
#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
pub struct TaskVerifierSpec {
#[serde(flatten)]
pub definition: VerifierTask,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VerifierTask {
Assertion(AssertionTask),
LlmJudge(LlmJudgeTask),
}
impl<'de> Deserialize<'de> for TaskVerifierSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let definition = VerifierTask::deserialize(deserializer)?;
let (depends_on, condition) = match &definition {
VerifierTask::Assertion(task) => (&task.depends_on, task.condition.is_some()),
VerifierTask::LlmJudge(task) => (&task.depends_on, task.condition.is_some()),
};
if !depends_on.is_empty() || condition {
return Err(serde::de::Error::custom(
"a task Verifier runs one check; depends_on and condition do not apply",
));
}
Ok(Self { definition })
}
}
#[cfg(feature = "server")]
fn eval_spec_openapi_schema() -> RefOr<Schema> {
let opaque_object = Schema::Object(ObjectBuilder::new().schema_type(Type::Object).build());
RefOr::T(Schema::Object(
ObjectBuilder::new()
.description(Some("Evaluation Verifier implementation spec."))
.property("dataset", opaque_object.clone())
.property("tasks", opaque_object.clone())
.property("workflow", opaque_object.clone())
.property("sampling", opaque_object.clone())
.property("pass_gate", opaque_object.clone())
.property("context_capture", opaque_object)
.required("tasks")
.schema_type(Type::Object)
.build(),
))
}
#[cfg(feature = "server")]
fn task_spec_openapi_schema() -> RefOr<Schema> {
RefOr::T(Schema::Object(
ObjectBuilder::new()
.description(Some(
"Task Verifier implementation spec: one assertion or llm_judge check.",
))
.property(
"kind",
Schema::Object(
ObjectBuilder::new()
.schema_type(Type::String)
.enum_values(Some(["assertion", "llm_judge"]))
.build(),
),
)
.required("kind")
.schema_type(Type::Object)
.build(),
))
}
#[cfg(feature = "server")]
fn implementation_branch(kind: &str, spec: RefOr<Schema>) -> RefOr<Schema> {
RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(Type::Object)
.property(
"kind",
Schema::Object(
ObjectBuilder::new()
.schema_type(Type::String)
.enum_values(Some([kind]))
.build(),
),
)
.required("kind")
.property("spec", spec)
.required("spec")
.build(),
))
}
#[cfg(feature = "server")]
impl PartialSchema for VerifierImplementation {
fn schema() -> RefOr<Schema> {
OneOfBuilder::new()
.item(implementation_branch(
"drift",
<DriftSpec as PartialSchema>::schema(),
))
.item(implementation_branch("eval", eval_spec_openapi_schema()))
.item(implementation_branch("task", task_spec_openapi_schema()))
.title(Some("VerifierImplementation"))
.description(Some("One typed Verifier implementation."))
.into()
}
}
#[cfg(feature = "server")]
impl ToSchema for VerifierImplementation {
fn name() -> Cow<'static, str> {
Cow::Borrowed("VerifierImplementation")
}
fn schemas(schemas: &mut Vec<(String, RefOr<Schema>)>) {
schemas.push((
<DriftSpec as ToSchema>::name().into(),
<DriftSpec as PartialSchema>::schema(),
));
<DriftSpec as ToSchema>::schemas(schemas);
}
}
impl VerifierImplementation {
#[must_use]
pub const fn kind_name(&self) -> &'static str {
match self {
Self::Drift(_) => "drift",
Self::Eval(_) => "eval",
Self::Task(_) => "task",
}
}
}
impl VerifierSpec {
pub fn validate(&self) -> Result<(), DriftValidationError> {
match &self.implementation {
VerifierImplementation::Drift(drift) => drift.validate(),
VerifierImplementation::Eval(_) | VerifierImplementation::Task(_) => Ok(()),
}
}
}
pub const OWNER_OCCURRENCE_KEY: &str = "$owner";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct VerificationStatus {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub binding_ids: Vec<BindingId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub baseline: Option<DriftBaselineStatus>,
}
impl VerificationStatus {
#[must_use]
pub fn derived(
binding_ids: Vec<BindingId>,
baseline: Option<DriftBaselineStatus>,
) -> Option<Self> {
(!binding_ids.is_empty() || baseline.is_some()).then_some(Self {
binding_ids,
baseline,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DriftBaselineState {
Pending,
Building,
Ready,
Failed,
}
impl DriftBaselineState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Building => "building",
Self::Ready => "ready",
Self::Failed => "failed",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"pending" => Some(Self::Pending),
"building" => Some(Self::Building),
"ready" => Some(Self::Ready),
"failed" => Some(Self::Failed),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct DriftBaselineStatus {
pub state: DriftBaselineState,
pub data: CardRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<VerificationError>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct VerificationBinding {
pub verifier: Ref,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runs_on: Option<InlineableRef<TriggerSpec>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on_failure: Vec<InlineableRef<OperatorSpec>>,
}