use crate::cli::args::OutputFormat;
use crate::exit_codes::{EXIT_CONFIG_ERROR, EXIT_SUCCESS, EXIT_TEST_FAILURE};
use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand};
use jsonschema::Draft;
use serde::Serialize;
use serde_json::Value;
use std::fmt;
use std::path::PathBuf;
const SCHEMA_LIST_REPORT: &str = "assay.evidence.schema.list.v1";
const SCHEMA_SHOW_REPORT: &str = "assay.evidence.schema.show.v1";
const SCHEMA_VALIDATION_REPORT: &str = "assay.evidence.schema.validation.v1";
macro_rules! include_packaged_schema {
($relative_path:literal) => {
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/receipt-schemas/",
$relative_path
))
};
}
const PROMPTFOO_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/promptfoo.assertion-component.v1.schema.json");
const OPENFEATURE_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/openfeature.evaluation-details.v1.schema.json");
const CYCLONEDX_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/cyclonedx.mlbom-model-component.v1.schema.json");
const MASTRA_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/mastra.score-event.v1.schema.json");
const PYDANTIC_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/pydantic.case-result.v1.schema.json");
const LIVEKIT_RECEIPT_SCHEMA: &str =
include_packaged_schema!("receipts/livekit.tool-action.v1.schema.json");
const PROMPTFOO_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/promptfoo-cli-jsonl-component-result.v1.schema.json");
const OPENFEATURE_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/openfeature-evaluation-details-export.v1.schema.json");
const CYCLONEDX_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/cyclonedx-mlbom-model-component-input.v1.schema.json");
const MASTRA_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/mastra-score-event-export.v1.schema.json");
const PYDANTIC_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/pydantic-case-result-export.v1.schema.json");
const LIVEKIT_INPUT_SCHEMA: &str =
include_packaged_schema!("inputs/livekit-function-tools-executed-export.v1.schema.json");
#[derive(Debug, Args, Clone)]
pub struct SchemaArgs {
#[command(subcommand)]
pub cmd: SchemaCmd,
}
#[derive(Debug, Subcommand, Clone)]
pub enum SchemaCmd {
List(SchemaListArgs),
Show(SchemaShowArgs),
Validate(SchemaValidateArgs),
}
#[derive(Debug, Args, Clone)]
pub struct SchemaListArgs {
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
}
#[derive(Debug, Args, Clone)]
pub struct SchemaShowArgs {
#[arg(value_name = "SCHEMA")]
pub schema: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
#[arg(long)]
pub raw: bool,
}
#[derive(Debug, Args, Clone)]
pub struct SchemaValidateArgs {
#[arg(long)]
pub schema: String,
#[arg(long)]
pub input: PathBuf,
#[arg(long)]
pub jsonl: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
enum SchemaKind {
Receipt,
Input,
}
impl fmt::Display for SchemaKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Receipt => f.write_str("receipt"),
Self::Input => f.write_str("input"),
}
}
}
#[derive(Clone, Copy)]
struct SchemaDescriptor {
name: &'static str,
aliases: &'static [&'static str],
kind: SchemaKind,
status: &'static str,
family: &'static str,
source_path: &'static str,
description: &'static str,
trust_basis_claim: Option<&'static str>,
importer_only: bool,
schema_json: &'static str,
}
impl SchemaDescriptor {
fn json_schema_value(&self) -> Result<Value> {
serde_json::from_str(self.schema_json)
.with_context(|| format!("failed to parse embedded schema {}", self.name))
}
fn json_schema_id(&self) -> Result<String> {
Ok(self
.json_schema_value()?
.get("$id")
.and_then(Value::as_str)
.unwrap_or(self.name)
.to_string())
}
fn matches(&self, needle: &str) -> Result<bool> {
if self.name == needle || self.source_path == needle || self.aliases.contains(&needle) {
return Ok(true);
}
Ok(self.json_schema_id()? == needle)
}
}
const SCHEMAS: &[SchemaDescriptor] = &[
SchemaDescriptor {
name: "promptfoo.assertion-component.v1",
aliases: &["assay.receipt.promptfoo.assertion-component.v1"],
kind: SchemaKind::Receipt,
status: "stable",
family: "external_eval_receipts",
source_path: "docs/reference/receipt-schemas/receipts/promptfoo.assertion-component.v1.schema.json",
description: "Assay receipt payload for one selected Promptfoo assertion component result.",
trust_basis_claim: Some("external_eval_receipt_boundary_visible"),
importer_only: false,
schema_json: PROMPTFOO_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "openfeature.evaluation-details.v1",
aliases: &["assay.receipt.openfeature.evaluation_details.v1"],
kind: SchemaKind::Receipt,
status: "stable",
family: "external_decision_receipts",
source_path: "docs/reference/receipt-schemas/receipts/openfeature.evaluation-details.v1.schema.json",
description: "Assay receipt payload for one bounded OpenFeature boolean EvaluationDetails decision.",
trust_basis_claim: Some("external_decision_receipt_boundary_visible"),
importer_only: false,
schema_json: OPENFEATURE_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "cyclonedx.mlbom-model-component.v1",
aliases: &["assay.receipt.cyclonedx.mlbom-model-component.v1"],
kind: SchemaKind::Receipt,
status: "stable",
family: "external_inventory_receipts",
source_path: "docs/reference/receipt-schemas/receipts/cyclonedx.mlbom-model-component.v1.schema.json",
description: "Assay receipt payload for one selected CycloneDX ML-BOM machine-learning-model component.",
trust_basis_claim: Some("external_inventory_receipt_boundary_visible"),
importer_only: false,
schema_json: CYCLONEDX_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "mastra.score-event.v1",
aliases: &["assay.receipt.mastra.score_event.v1"],
kind: SchemaKind::Receipt,
status: "experimental",
family: "score_receipts",
source_path: "docs/reference/receipt-schemas/receipts/mastra.score-event.v1.schema.json",
description: "Assay receipt payload for one bounded Mastra ScoreEvent-derived score artifact.",
trust_basis_claim: None,
importer_only: true,
schema_json: MASTRA_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "pydantic.case-result.v1",
aliases: &["assay.receipt.pydantic.case_result.v1"],
kind: SchemaKind::Receipt,
status: "experimental",
family: "case_result_receipts",
source_path: "docs/reference/receipt-schemas/receipts/pydantic.case-result.v1.schema.json",
description: "Assay receipt payload for one bounded Pydantic Evals reduced case-result artifact.",
trust_basis_claim: None,
importer_only: true,
schema_json: PYDANTIC_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "livekit.tool-action.v1",
aliases: &["assay.receipt.livekit.tool-action.v1"],
kind: SchemaKind::Receipt,
status: "experimental",
family: "acted_receipts_candidate",
source_path: "docs/reference/receipt-schemas/receipts/livekit.tool-action.v1.schema.json",
description: "Assay receipt payload for one bounded LiveKit function tool action artifact.",
trust_basis_claim: None,
importer_only: true,
schema_json: LIVEKIT_RECEIPT_SCHEMA,
},
SchemaDescriptor {
name: "promptfoo-cli-jsonl-component-result.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "stable",
family: "external_eval_receipts",
source_path: "docs/reference/receipt-schemas/inputs/promptfoo-cli-jsonl-component-result.v1.schema.json",
description: "Supported Promptfoo CLI JSONL row shape containing assertion component results.",
trust_basis_claim: Some("external_eval_receipt_boundary_visible"),
importer_only: false,
schema_json: PROMPTFOO_INPUT_SCHEMA,
},
SchemaDescriptor {
name: "openfeature-evaluation-details-export.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "stable",
family: "external_decision_receipts",
source_path: "docs/reference/receipt-schemas/inputs/openfeature-evaluation-details-export.v1.schema.json",
description: "Supported reduced OpenFeature boolean EvaluationDetails JSONL row shape.",
trust_basis_claim: Some("external_decision_receipt_boundary_visible"),
importer_only: false,
schema_json: OPENFEATURE_INPUT_SCHEMA,
},
SchemaDescriptor {
name: "cyclonedx-mlbom-model-component-input.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "stable",
family: "external_inventory_receipts",
source_path: "docs/reference/receipt-schemas/inputs/cyclonedx-mlbom-model-component-input.v1.schema.json",
description: "Supported CycloneDX ML-BOM input shape for selecting one machine-learning-model component.",
trust_basis_claim: Some("external_inventory_receipt_boundary_visible"),
importer_only: false,
schema_json: CYCLONEDX_INPUT_SCHEMA,
},
SchemaDescriptor {
name: "mastra-score-event-export.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "experimental",
family: "score_receipts",
source_path: "docs/reference/receipt-schemas/inputs/mastra-score-event-export.v1.schema.json",
description: "Supported reduced Mastra ScoreEvent JSONL row shape.",
trust_basis_claim: None,
importer_only: true,
schema_json: MASTRA_INPUT_SCHEMA,
},
SchemaDescriptor {
name: "pydantic-case-result-export.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "experimental",
family: "case_result_receipts",
source_path: "docs/reference/receipt-schemas/inputs/pydantic-case-result-export.v1.schema.json",
description: "Supported reduced Pydantic Evals case-result JSONL row shape.",
trust_basis_claim: None,
importer_only: true,
schema_json: PYDANTIC_INPUT_SCHEMA,
},
SchemaDescriptor {
name: "livekit-function-tools-executed-export.v1",
aliases: &[],
kind: SchemaKind::Input,
status: "experimental",
family: "acted_receipts_candidate",
source_path: "docs/reference/receipt-schemas/inputs/livekit-function-tools-executed-export.v1.schema.json",
description: "Supported reduced LiveKit FunctionToolsExecutedEvent artifact shape.",
trust_basis_claim: None,
importer_only: true,
schema_json: LIVEKIT_INPUT_SCHEMA,
},
];
#[derive(Debug, Serialize)]
struct SchemaListReport {
schema: &'static str,
schemas: Vec<SchemaMetadata>,
}
#[derive(Debug, Serialize)]
struct SchemaShowReport {
schema: &'static str,
metadata: SchemaMetadata,
}
#[derive(Debug, Serialize)]
struct SchemaMetadata {
name: String,
kind: SchemaKind,
status: String,
family: String,
json_schema_id: String,
source_path: String,
description: String,
trust_basis_claim: Option<String>,
importer_only: bool,
aliases: Vec<String>,
}
#[derive(Debug, Serialize)]
struct SchemaValidationReport {
schema: &'static str,
schema_name: String,
schema_kind: SchemaKind,
input: String,
jsonl: bool,
valid: bool,
documents: usize,
errors: Vec<SchemaValidationError>,
}
#[derive(Debug, Serialize)]
struct SchemaValidationError {
document: usize,
kind: SchemaValidationErrorKind,
instance_path: String,
message: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
enum SchemaValidationErrorKind {
Parse,
EmptyInput,
Schema,
}
impl SchemaValidationReport {
fn has_input_errors(&self) -> bool {
self.errors.iter().any(|error| {
matches!(
error.kind,
SchemaValidationErrorKind::Parse | SchemaValidationErrorKind::EmptyInput
)
})
}
}
pub fn cmd_schema(args: SchemaArgs) -> Result<i32> {
match args.cmd {
SchemaCmd::List(args) => cmd_list(args),
SchemaCmd::Show(args) => cmd_show(args),
SchemaCmd::Validate(args) => cmd_validate(args),
}
}
fn cmd_list(args: SchemaListArgs) -> Result<i32> {
let report = SchemaListReport {
schema: SCHEMA_LIST_REPORT,
schemas: SCHEMAS
.iter()
.map(schema_metadata)
.collect::<Result<Vec<_>>>()?,
};
match args.format {
OutputFormat::Text => write_list_text(&report),
OutputFormat::Json => write_json(&report),
}
}
fn cmd_show(args: SchemaShowArgs) -> Result<i32> {
let descriptor = find_schema(&args.schema)?;
if args.raw {
let raw = descriptor.json_schema_value()?;
println!("{}", serde_json::to_string_pretty(&raw)?);
return Ok(EXIT_SUCCESS);
}
let report = SchemaShowReport {
schema: SCHEMA_SHOW_REPORT,
metadata: schema_metadata(descriptor)?,
};
match args.format {
OutputFormat::Text => write_show_text(&report),
OutputFormat::Json => write_json(&report),
}
}
fn cmd_validate(args: SchemaValidateArgs) -> Result<i32> {
let descriptor = find_schema(&args.schema)?;
let input = std::fs::read_to_string(&args.input).with_context(|| {
format!(
"failed to read schema validation input {}",
args.input.display()
)
})?;
let schema_value = descriptor.json_schema_value()?;
let validator = jsonschema::options()
.with_draft(Draft::Draft202012)
.build(&schema_value)
.with_context(|| format!("failed to compile schema {}", descriptor.name))?;
let report = validate_input(descriptor, &args.input, args.jsonl, &input, &validator);
match args.format {
OutputFormat::Text => write_validation_text(&report),
OutputFormat::Json => write_json(&report),
}?;
if report.valid {
Ok(EXIT_SUCCESS)
} else if report.has_input_errors() {
Ok(EXIT_CONFIG_ERROR)
} else {
Ok(EXIT_TEST_FAILURE)
}
}
fn find_schema(raw: &str) -> Result<&'static SchemaDescriptor> {
let needle = raw.trim();
if needle.is_empty() {
bail!("schema name is empty");
}
for descriptor in SCHEMAS {
if descriptor.matches(needle)? {
return Ok(descriptor);
}
}
bail!("unknown schema {needle:?}; run `assay evidence schema list` for supported schemas");
}
fn schema_metadata(descriptor: &SchemaDescriptor) -> Result<SchemaMetadata> {
Ok(SchemaMetadata {
name: descriptor.name.to_string(),
kind: descriptor.kind,
status: descriptor.status.to_string(),
family: descriptor.family.to_string(),
json_schema_id: descriptor.json_schema_id()?,
source_path: descriptor.source_path.to_string(),
description: descriptor.description.to_string(),
trust_basis_claim: descriptor.trust_basis_claim.map(str::to_string),
importer_only: descriptor.importer_only,
aliases: descriptor
.aliases
.iter()
.map(|alias| (*alias).to_string())
.collect(),
})
}
fn validate_input(
descriptor: &SchemaDescriptor,
input_path: &std::path::Path,
jsonl: bool,
input: &str,
validator: &jsonschema::Validator,
) -> SchemaValidationReport {
let mut errors = Vec::new();
let mut documents = 0usize;
if jsonl {
for (idx, line) in input.lines().enumerate() {
let line_number = idx + 1;
if line.trim().is_empty() {
continue;
}
documents += 1;
match serde_json::from_str::<Value>(line) {
Ok(value) => collect_validation_errors(line_number, &value, validator, &mut errors),
Err(err) => errors.push(SchemaValidationError {
document: line_number,
kind: SchemaValidationErrorKind::Parse,
instance_path: "$".to_string(),
message: format!("invalid JSON: {err}"),
}),
}
}
} else {
documents = 1;
match serde_json::from_str::<Value>(input) {
Ok(value) => collect_validation_errors(1, &value, validator, &mut errors),
Err(err) => errors.push(SchemaValidationError {
document: 1,
kind: SchemaValidationErrorKind::Parse,
instance_path: "$".to_string(),
message: format!("invalid JSON: {err}"),
}),
}
}
if documents == 0 {
errors.push(SchemaValidationError {
document: 0,
kind: SchemaValidationErrorKind::EmptyInput,
instance_path: "$".to_string(),
message: "no JSON documents found".to_string(),
});
}
SchemaValidationReport {
schema: SCHEMA_VALIDATION_REPORT,
schema_name: descriptor.name.to_string(),
schema_kind: descriptor.kind,
input: input_path.display().to_string(),
jsonl,
valid: errors.is_empty(),
documents,
errors,
}
}
fn collect_validation_errors(
document: usize,
value: &Value,
validator: &jsonschema::Validator,
errors: &mut Vec<SchemaValidationError>,
) {
errors.extend(
validator
.iter_errors(value)
.map(|err| SchemaValidationError {
document,
kind: SchemaValidationErrorKind::Schema,
instance_path: err.instance_path().to_string(),
message: err.to_string(),
}),
);
}
fn write_list_text(report: &SchemaListReport) -> Result<i32> {
println!("Assay Evidence Schema Registry");
println!("Schema: {}", report.schema);
for schema in &report.schemas {
println!(
"- {} [{}; {}; family: {}]",
schema.name, schema.kind, schema.status, schema.family
);
println!(" id: {}", schema.json_schema_id);
println!(" source: {}", schema.source_path);
println!(" description: {}", schema.description);
if let Some(claim) = &schema.trust_basis_claim {
println!(" trust_basis_claim: {claim}");
} else if schema.importer_only {
println!(" trust_basis_claim: none (importer-only)");
}
}
Ok(EXIT_SUCCESS)
}
fn write_show_text(report: &SchemaShowReport) -> Result<i32> {
let schema = &report.metadata;
println!("Schema: {}", schema.name);
println!("Kind: {}", schema.kind);
println!("Status: {}", schema.status);
println!("Family: {}", schema.family);
println!("JSON Schema $id: {}", schema.json_schema_id);
println!("Source: {}", schema.source_path);
println!("Description: {}", schema.description);
if let Some(claim) = &schema.trust_basis_claim {
println!("Trust Basis claim: {claim}");
} else if schema.importer_only {
println!("Trust Basis claim: none (importer-only)");
} else {
println!("Trust Basis claim: none");
}
if !schema.aliases.is_empty() {
println!("Aliases:");
for alias in &schema.aliases {
println!("- {alias}");
}
}
Ok(EXIT_SUCCESS)
}
fn write_validation_text(report: &SchemaValidationReport) -> Result<i32> {
if report.valid {
println!(
"Schema validation passed: {} matches {} ({} document(s))",
report.input, report.schema_name, report.documents
);
return Ok(EXIT_SUCCESS);
}
println!(
"Schema validation failed: {} does not match {}",
report.input, report.schema_name
);
for error in &report.errors {
println!(
"- document {} at {}: {}",
error.document, error.instance_path, error.message
);
}
Ok(EXIT_SUCCESS)
}
fn write_json<T: Serialize>(value: &T) -> Result<i32> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(EXIT_SUCCESS)
}