#![allow(
clippy::print_stdout,
clippy::print_stderr,
reason = "this IS the CLI: stdout carries the run report and stderr the \
diagnostics; only library crates are restricted to `tracing`"
)]
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use veredictum::pipeline::assets::{
conformance_assets, performance_assets, schema_files, stress_overlay,
};
use veredictum::pipeline::catalogue::{coverage_report_path, validate_tree, write_coverage_report};
use veredictum::pipeline::conformance::{RunRequest, RunWarning, execute_run};
use veredictum::pipeline::judgement::{JudgementRequest, judge};
use veredictum::pipeline::measured::{
MeasuredEvent, MeasuredRequest, ProbeRequest, StressRequest, SustainedWindow, run_aql_probe,
run_measured, run_stress,
};
use veredictum::pipeline::{Error, RenderedFile, ensure_parent_dir, to_json_document, write_file};
#[derive(Parser)]
#[command(
name = "veredictum",
about = "The independent conformance instrument for openEHR clinical data repositories",
version
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
EmitSchemas {
#[arg(long)]
out: PathBuf,
},
Run {
#[arg(long)]
root: PathBuf,
#[arg(long)]
ixit: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = "ferroehr")]
sut_name: String,
#[arg(long, default_value = "dev")]
sut_version: String,
#[arg(long)]
filter: Option<String>,
#[arg(long)]
statement: Option<PathBuf>,
},
Validate {
#[arg(long)]
root: PathBuf,
#[arg(long)]
specs: Option<PathBuf>,
#[arg(long)]
write_report: bool,
},
Perf {
#[arg(long)]
root: PathBuf,
#[arg(long)]
ixit: PathBuf,
#[arg(long)]
results: PathBuf,
#[arg(long)]
class: String,
#[arg(long, default_value_t = 16)]
seed_workers: usize,
#[arg(long, default_value_t = 1)]
hours: u64,
},
Stress {
#[arg(long)]
root: PathBuf,
#[arg(long)]
ixit: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = "POC")]
corpus_class: String,
#[arg(long, default_value_t = 16)]
seed_workers: usize,
#[arg(long, default_value_t = 120)]
step_secs: u64,
#[arg(long, default_value_t = 3)]
bisections: u32,
#[arg(long, default_value_t = 4096.0)]
max_rate: f64,
},
AqlProbe {
#[arg(long)]
root: PathBuf,
#[arg(long)]
ixit: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = "POC")]
corpus_class: String,
#[arg(long, default_value_t = 16)]
seed_workers: usize,
#[arg(long, default_value_t = 20)]
requests: u32,
},
StressCompare {
#[arg(long)]
left: PathBuf,
#[arg(long)]
left_label: String,
#[arg(long)]
right: PathBuf,
#[arg(long)]
right_label: String,
#[arg(long)]
out: PathBuf,
},
PerfAssets {
#[arg(long)]
root: PathBuf,
#[arg(long)]
results: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long)]
summary: Option<PathBuf>,
#[arg(long)]
stress: Option<PathBuf>,
},
ConformanceAssets {
#[arg(long)]
root: PathBuf,
#[arg(long)]
results: PathBuf,
#[arg(long)]
verdicts: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = "")]
suffix: String,
},
Verdicts {
#[arg(long)]
statement: PathBuf,
#[arg(long)]
results: PathBuf,
#[arg(long)]
root: PathBuf,
#[arg(long)]
out: PathBuf,
},
}
fn fail(e: &Error) -> ExitCode {
eprintln!("{e}");
ExitCode::from(2)
}
fn emit(out: &Path, files: &[RenderedFile]) -> Result<(), ExitCode> {
if let Err(e) = std::fs::create_dir_all(out) {
eprintln!("cannot create {}: {e}", out.display());
return Err(ExitCode::from(2));
}
for file in files {
let path = out.join(&file.name);
if let Err(e) = std::fs::write(&path, &file.body) {
eprintln!("cannot write {}: {e}", path.display());
return Err(ExitCode::from(2));
}
println!("wrote {}", path.display());
}
Ok(())
}
fn emit_one(path: &Path, body: &str) -> Result<(), ExitCode> {
if let Err(e) = ensure_parent_dir(path) {
eprintln!("{e}");
return Err(ExitCode::from(2));
}
if let Err(e) = write_file(path, body) {
eprintln!("{e}");
return Err(ExitCode::from(2));
}
Ok(())
}
#[expect(
clippy::too_many_lines,
reason = "the dispatch table: one arm per subcommand, each binding its own flags"
)]
fn main() -> ExitCode {
match Cli::parse().command {
Command::EmitSchemas { out } => emit_schemas_command(&out),
Command::Run {
root,
ixit,
out,
sut_name,
sut_version,
filter,
statement,
} => run_command(&RunRequest {
root: &root,
ixit: &ixit,
out_dir: &out,
sut_name: &sut_name,
sut_version: &sut_version,
filter: filter.as_deref(),
statement: statement.as_deref(),
}),
Command::Validate {
root,
specs,
write_report,
} => validate_command(&root, specs.as_deref(), write_report),
Command::Perf {
root,
ixit,
results,
class,
seed_workers,
hours,
} => perf_command(&root, &ixit, &results, &class, seed_workers, hours),
Command::Stress {
root,
ixit,
out,
corpus_class,
seed_workers,
step_secs,
bisections,
max_rate,
} => stress_command(
&StressRequest {
root: &root,
ixit: &ixit,
corpus_class: &corpus_class,
seed_workers,
step_secs,
bisections,
max_rate,
},
&out,
),
Command::AqlProbe {
root,
ixit,
out,
corpus_class,
seed_workers,
requests,
} => probe_command(
&ProbeRequest {
root: &root,
ixit: &ixit,
corpus_class: &corpus_class,
seed_workers,
requests,
},
&out,
),
Command::StressCompare {
left,
left_label,
right,
right_label,
out,
} => stress_compare_command(&left, &left_label, &right, &right_label, &out),
Command::PerfAssets {
root,
results,
out,
summary,
stress,
} => perf_assets_command(&root, &results, &out, summary.as_deref(), stress.as_deref()),
Command::ConformanceAssets {
root,
results,
verdicts,
out,
suffix,
} => conformance_assets_command(&root, &results, &verdicts, &out, &suffix),
Command::Verdicts {
statement,
results,
root,
out,
} => verdicts_command(
&JudgementRequest {
statement: &statement,
results: &results,
root: &root,
},
&out,
),
}
}
fn emit_schemas_command(out: &Path) -> ExitCode {
match emit(out, &schema_files()) {
Ok(()) => ExitCode::SUCCESS,
Err(code) => code,
}
}
fn validate_command(root: &Path, specs: Option<&Path>, write_report: bool) -> ExitCode {
let validation = match validate_tree(root, specs) {
Ok(validation) => validation,
Err(e) => return fail(&e),
};
for finding in &validation.findings {
println!("{finding}");
}
if write_report
&& let Some(specs) = specs
&& let Some(path) = coverage_report_path(specs)
{
match write_coverage_report(&validation.loaded.set, specs, &path) {
Ok(()) => println!("wrote {}", path.display()),
Err(e) => eprintln!("warning: {e}"),
}
}
println!(
"{} case(s), {} binding(s), {} party statement(s), {} finding(s)",
validation.loaded.set.cases.len(),
validation.loaded.set.bindings.len(),
validation.loaded.set.parties.len(),
validation.findings.len()
);
if validation.is_clean() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn verdicts_command(request: &JudgementRequest<'_>, out: &Path) -> ExitCode {
let judgement = match judge(request) {
Ok(judgement) => judgement,
Err(e) => return fail(&e),
};
if let Err(code) = emit(out, &judgement.documents) {
return code;
}
for finding in &judgement.report.review {
println!("static-review: {}", finding.message);
}
println!(
"{} capability verdict(s), {} of {} cases driven, {} review finding(s)",
judgement.report.capabilities.len(),
judgement.report.coverage.driven,
judgement.report.coverage.selected,
judgement.report.review.len(),
);
if judgement.is_clean() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn conformance_assets_command(
root: &Path,
results: &Path,
verdicts: &Path,
out: &Path,
suffix: &str,
) -> ExitCode {
let files = match conformance_assets(root, results, verdicts, suffix) {
Ok(files) => files,
Err(e) => return fail(&e),
};
match emit(out, &files) {
Ok(()) => ExitCode::SUCCESS,
Err(code) => code,
}
}
fn perf_assets_command(
root: &Path,
results: &Path,
out: &Path,
summary: Option<&Path>,
stress: Option<&Path>,
) -> ExitCode {
let assets = match performance_assets(root, results, stress) {
Ok(assets) => assets,
Err(e) => return fail(&e),
};
if let Err(code) = emit(out, &assets.files) {
return code;
}
if let Some(path) = summary {
let body = match assets.summary_markdown() {
Ok(body) => body,
Err(e) => return fail(&e),
};
if let Err(code) = emit_one(path, &body) {
return code;
}
println!("wrote {}", path.display());
}
ExitCode::SUCCESS
}
fn stress_compare_command(
left: &Path,
left_label: &str,
right: &Path,
right_label: &str,
out: &Path,
) -> ExitCode {
let svg = match stress_overlay((left_label, left), (right_label, right)) {
Ok(svg) => svg,
Err(e) => return fail(&e),
};
if let Err(code) = emit_one(out, &svg) {
return code;
}
println!("wrote {}", out.display());
ExitCode::SUCCESS
}
fn stress_command(request: &StressRequest<'_>, out: &Path) -> ExitCode {
let progress = |message: String| eprintln!("[stress] {message}");
let report = match run_stress(request, &progress) {
Ok(report) => report,
Err(e) => return fail(&e),
};
let document = match to_json_document(&report, "serialize") {
Ok(document) => document,
Err(e) => return fail(&e),
};
if let Err(code) = emit_one(out, &document) {
return code;
}
println!("{}", report.remark);
println!(
"wrote {} ({} steps, max sustainable {:.1}/s)",
out.display(),
report.steps.len(),
report.max_sustainable_throughput_per_s
);
ExitCode::SUCCESS
}
fn probe_command(request: &ProbeRequest<'_>, out: &Path) -> ExitCode {
let progress = |message: String| eprintln!("[probe] {message}");
let report = match run_aql_probe(request, &progress) {
Ok(report) => report,
Err(e) => return fail(&e),
};
let document = match to_json_document(&report, "serialize") {
Ok(document) => document,
Err(e) => return fail(&e),
};
if let Err(code) = emit_one(out, &document) {
return code;
}
println!("wrote {} ({} probes)", out.display(), report.probes.len());
ExitCode::SUCCESS
}
fn perf_command(
root: &Path,
ixit: &Path,
results: &Path,
class_token: &str,
seed_workers: usize,
hours: u64,
) -> ExitCode {
let Some(window) = SustainedWindow::hours(hours) else {
eprintln!("--hours must be one of 1 | 2 | 4 | 6 | 8 | 12 (got {hours})");
return ExitCode::from(2);
};
let request = MeasuredRequest {
root,
ixit,
results,
class: class_token,
seed_workers,
window,
};
let observe = |event: MeasuredEvent<'_>| match event {
MeasuredEvent::Progress(message) => eprintln!("[perf] {message}"),
MeasuredEvent::CaseStarted { case, source } => println!(
"case {} (class {class_token}) from {}",
case.id,
source.display()
),
MeasuredEvent::Measured(measurement) => {
for op in &measurement.operations {
println!(
" {}: {} requests, {} errors, p50 {:.1}ms p90 {:.1}ms p99 {:.1}ms",
op.operation,
op.requests,
op.errors,
op.latency_ms_p50,
op.latency_ms_p90,
op.latency_ms_p99
);
}
println!(" {}", veredictum::perf::verdict_evidence(measurement));
}
MeasuredEvent::PrunedOrphan(case) => {
println!(" pruned orphaned measurement for retired case {case}");
}
MeasuredEvent::Merged(path) => {
println!(" measurement merged into {}", path.display());
}
};
match run_measured(&request, &observe) {
Ok(run) if run.earned_all => ExitCode::SUCCESS,
Ok(_) => ExitCode::from(1),
Err(e) => fail(&e),
}
}
fn run_command(request: &RunRequest<'_>) -> ExitCode {
let warn = |warning: RunWarning<'_>| match warning {
RunWarning::CarriedMeasurements {
count,
measured_at,
running_at,
} => eprintln!(
"warning: carrying {count} measurement record(s) taken at SUT version {measured_at} into a run at {running_at} — re-measure or attest the surface unchanged"
),
};
let outcome = match execute_run(request, &warn) {
Ok(outcome) => outcome,
Err(e) => return fail(&e),
};
if let Err(e) = std::fs::create_dir_all(request.out_dir) {
eprintln!("cannot create {}: {e}", request.out_dir.display());
return ExitCode::from(2);
}
let document = match outcome.results_document() {
Ok(document) => document,
Err(e) => return fail(&e),
};
if let Err(e) = write_file(&outcome.results_path, &document) {
return fail(&e);
}
let exceptions = match outcome.exceptions_document() {
Ok(document) => document,
Err(e) => return fail(&e),
};
if let Err(e) = write_file(&outcome.exceptions_path, &exceptions) {
return fail(&e);
}
println!(
"{} case-records: {} passed / {} failed / {} errored / {} n-a; interpreter coverage {:.1}% ({} exceptions); wrote {}",
outcome.report.records.len(),
outcome.counts.passed,
outcome.counts.failed,
outcome.counts.errored,
outcome.counts.not_applicable,
outcome.report.interpreter_coverage() * 100.0,
outcome.report.exceptions.len(),
outcome.results_path.display()
);
if outcome.is_clean() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}