use clap::{Parser, Subcommand};
use serde::Serialize;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(
name = "etdl",
version,
about = "ETDL parser, validator, and compiler",
after_help = "Exit codes: 0 = success, 1 = validation/compile failure, 2 = usage error"
)]
struct Cli {
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
quiet: bool,
#[arg(long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Compile {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
#[arg(
long,
default_value = "rust",
help = "Target language for code generation"
)]
target: String,
#[arg(
long,
default_value = ".",
help = "Output directory for generated code"
)]
out_dir: PathBuf,
#[arg(
long = "library-path",
help = "Additional search path for optional (non-std.*) libraries; repeatable"
)]
library_path: Vec<PathBuf>,
},
Validate {
#[arg(help = "Path(s) to .etdl document(s) or directories")]
files: Vec<PathBuf>,
#[arg(
long = "library-path",
help = "Additional search path for optional (non-std.*) libraries; repeatable"
)]
library_path: Vec<PathBuf>,
},
Analyze {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
#[arg(
long,
help = "Path to a dependency model (.yaml/.json); enables dependency-aware analysis"
)]
dependencies: Option<PathBuf>,
#[arg(
long,
help = "Run Monte Carlo uncertainty propagation with this many samples"
)]
monte_carlo: Option<usize>,
#[arg(
long,
default_value_t = 42,
help = "Seed for Monte Carlo (reproducibility)"
)]
seed: u64,
#[arg(
long,
help = "Path to declared input uncertainties (.yaml/.json): event id -> sampling law"
)]
uncertainty: Option<PathBuf>,
#[arg(
long,
default_value_t = 0.95,
help = "Central interval level for the propagated uncertainty"
)]
level: f64,
#[arg(
long,
default_value_t = 1e-3,
help = "Absolute perturbation size for sensitivity analysis"
)]
perturbation: f64,
#[arg(long, help = "Skip importance analysis")]
no_importance: bool,
#[arg(long, help = "Skip sensitivity analysis")]
no_sensitivity: bool,
#[arg(
long,
help = "Rank inputs by how much of the output uncertainty each accounts for \
(costs one extra propagation run per uncertain input)"
)]
uncertainty_ranking: bool,
#[arg(long, help = "Write the analysis result artifact (JSON) to this path")]
output: Option<PathBuf>,
#[arg(
long = "library-path",
help = "Additional search path for optional (non-std.*) libraries; repeatable"
)]
library_path: Vec<PathBuf>,
},
Discover {
#[arg(help = "Path to a source file or directory to analyze")]
path: PathBuf,
#[arg(
long,
default_value = "rust",
help = "Source language (only 'rust' is implemented)"
)]
language: String,
#[arg(
long,
default_value = "text",
help = "Output format: text | json | yaml"
)]
format: String,
#[arg(
long,
default_value_t = 0.5,
help = "Minimum discovery confidence (0.0-1.0); filters candidates"
)]
min_confidence: f64,
#[arg(long, help = "Write the report to this file instead of stdout")]
output: Option<PathBuf>,
#[arg(long, help = "Exclude a path (repeatable)")]
exclude: Vec<PathBuf>,
#[arg(
long,
default_value = "auto",
help = "Ontology mapping policy: auto | conservative | off"
)]
ontology_policy: String,
},
Reliability {
#[command(subcommand)]
command: ReliabilityCommand,
},
Library {
#[command(subcommand)]
command: LibraryCommand,
},
Tree {
#[command(subcommand)]
command: TreeCommand,
},
Capabilities,
Conformance {
#[command(subcommand)]
command: ConformanceCommand,
},
Version,
}
#[derive(clap::Subcommand)]
enum ConformanceCommand {
Status,
Manifest,
}
#[derive(clap::Subcommand)]
enum LibraryCommand {
List,
Resolve {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
#[arg(
long = "library-path",
help = "Additional search path for optional (non-std.*) libraries; repeatable"
)]
library_path: Vec<PathBuf>,
},
}
#[derive(clap::Subcommand)]
enum TreeCommand {
Validate {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
},
Inspect {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
},
}
#[derive(clap::Subcommand)]
enum ReliabilityCommand {
Resolve {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
},
Validate {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
},
Inspect {
#[arg(help = "Path to a reliability artifact (.rprob/.yaml/.json)")]
file: PathBuf,
},
Estimate {
#[arg(help = "Path to an observations file (.yaml/.json)")]
file: PathBuf,
#[arg(
long,
default_value = "empirical",
help = "Estimator: empirical | beta-binomial | exponential"
)]
method: String,
#[arg(
long,
default_value_t = 0.95,
help = "Interval confidence/credibility level"
)]
level: f64,
#[arg(long, default_value_t = 1.0, help = "Beta-binomial prior alpha")]
prior_alpha: f64,
#[arg(long, default_value_t = 1.0, help = "Beta-binomial prior beta")]
prior_beta: f64,
#[arg(long, help = "Mission time for the exponential estimator")]
mission_time: Option<f64>,
#[arg(long, help = "Output artifact path (.rprob/.yaml/.json)")]
output: Option<PathBuf>,
},
Compare {
#[arg(help = "Path to the 'before' analysis result (.json)")]
before: PathBuf,
#[arg(help = "Path to the 'after' analysis result (.json)")]
after: PathBuf,
},
Trace {
#[arg(help = "Path to a reliability artifact (.rprob/.yaml/.json)")]
file: PathBuf,
#[arg(help = "Estimate id to trace")]
estimate: String,
},
Calibrate {
#[arg(help = "Path to a reliability artifact (.rprob/.yaml/.json)")]
artifact: PathBuf,
#[arg(help = "Failure-mode/event id to calibrate")]
event: String,
#[arg(
long = "dataset",
required = true,
help = "Path to an observation dataset (.yaml/.json); repeatable"
)]
dataset: Vec<PathBuf>,
#[arg(long, default_value_t = 0.05, help = "Significance level for 'potential deviation'")]
alpha: f64,
#[arg(
long,
default_value_t = 0.01,
help = "Stricter significance level for 'significant deviation' (drift)"
)]
strict_alpha: f64,
#[arg(
long,
default_value_t = 20,
help = "Minimum exposure below which the result is 'insufficient data'"
)]
min_exposure: u64,
#[arg(long, help = "Write the calibration result (JSON) to this path")]
output: Option<PathBuf>,
},
}
fn main() {
let Cli {
json,
quiet,
verbose,
command,
} = Cli::parse();
let flags = CliFlags {
json,
quiet,
verbose,
};
let code = match command {
Command::Compile {
file,
target,
out_dir,
library_path,
} => cmd_compile(&flags, &file, &target, &out_dir, &library_path),
Command::Validate { files, library_path } => cmd_validate(&flags, &files, &library_path),
Command::Analyze {
file,
dependencies,
monte_carlo,
seed,
uncertainty,
level,
perturbation,
no_importance,
no_sensitivity,
uncertainty_ranking,
output,
library_path,
} => {
let args = AnalyzeArgs {
file,
dependencies,
monte_carlo,
seed,
uncertainty,
level,
perturbation,
importance: !no_importance,
sensitivity: !no_sensitivity,
uncertainty_ranking,
output,
library_path,
};
cmd_analyze(&flags, &args)
}
Command::Discover {
path,
language,
format,
min_confidence,
output,
exclude,
ontology_policy,
} => {
let args = DiscoverArgs {
path,
language,
format,
min_confidence,
output,
exclude,
ontology_policy,
};
#[cfg(feature = "discovery")]
{
cmd_discover(&flags, &args)
}
#[cfg(not(feature = "discovery"))]
{
let _ = (args,);
capability_unavailable(
"failure discovery",
"rebuild etdl-cli with the 'discovery' feature",
);
1
}
}
Command::Reliability { command } => match command {
ReliabilityCommand::Resolve { file } => {
#[cfg(feature = "reliability")]
{
cmd_reliability_resolve(&flags, &file)
}
#[cfg(not(feature = "reliability"))]
{
let _ = file;
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Validate { file } => {
#[cfg(feature = "reliability")]
{
cmd_reliability_validate(&flags, &file)
}
#[cfg(not(feature = "reliability"))]
{
let _ = file;
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Inspect { file } => {
#[cfg(feature = "reliability")]
{
cmd_reliability_inspect(&flags, &file)
}
#[cfg(not(feature = "reliability"))]
{
let _ = file;
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Estimate {
file,
method,
level,
prior_alpha,
prior_beta,
mission_time,
output,
} => {
let args = EstimateArgs {
file,
method,
level,
prior_alpha,
prior_beta,
mission_time,
output,
};
#[cfg(feature = "reliability")]
{
cmd_reliability_estimate(&flags, &args)
}
#[cfg(not(feature = "reliability"))]
{
let _ = args;
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Compare { before, after } => {
#[cfg(feature = "reliability")]
{
cmd_reliability_compare(&flags, &before, &after)
}
#[cfg(not(feature = "reliability"))]
{
let _ = (before, after);
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Trace { file, estimate } => {
#[cfg(feature = "reliability")]
{
cmd_reliability_trace(&flags, &file, &estimate)
}
#[cfg(not(feature = "reliability"))]
{
let _ = (file, estimate);
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
ReliabilityCommand::Calibrate {
artifact,
event,
dataset,
alpha,
strict_alpha,
min_exposure,
output,
} => {
let args = CalibrateArgs {
artifact,
event,
dataset,
alpha,
strict_alpha,
min_exposure,
output,
};
#[cfg(feature = "reliability")]
{
cmd_reliability_calibrate(&flags, &args)
}
#[cfg(not(feature = "reliability"))]
{
let _ = args;
capability_unavailable(
"reliability",
"rebuild etdl-cli with the 'reliability' feature",
);
1
}
}
},
Command::Library { command } => match command {
LibraryCommand::List => cmd_library_list(&flags),
LibraryCommand::Resolve { file, library_path } => {
cmd_library_resolve(&flags, &file, &library_path)
}
},
Command::Tree { command } => match command {
TreeCommand::Validate { file } => cmd_tree_validate(&flags, &file),
TreeCommand::Inspect { file } => cmd_tree_inspect(&flags, &file),
},
Command::Capabilities => cmd_capabilities(&flags),
Command::Conformance { command } => match command {
ConformanceCommand::Status => cmd_conformance_status(&flags),
ConformanceCommand::Manifest => cmd_conformance_manifest(&flags),
},
Command::Version => {
if flags.json {
println!(
"{}",
serde_json::json!({ "name": "etdl", "version": env!("CARGO_PKG_VERSION") })
);
} else {
println!("etdl {}", env!("CARGO_PKG_VERSION"));
}
0
}
};
std::process::exit(code);
}
#[derive(Clone, Copy)]
struct CliFlags {
json: bool,
quiet: bool,
verbose: bool,
}
#[allow(dead_code)] struct DiscoverArgs {
path: PathBuf,
language: String,
format: String,
min_confidence: f64,
output: Option<PathBuf>,
exclude: Vec<PathBuf>,
ontology_policy: String,
}
#[allow(dead_code)] struct AnalyzeArgs {
file: PathBuf,
dependencies: Option<PathBuf>,
monte_carlo: Option<usize>,
seed: u64,
uncertainty: Option<PathBuf>,
level: f64,
perturbation: f64,
importance: bool,
sensitivity: bool,
uncertainty_ranking: bool,
output: Option<PathBuf>,
library_path: Vec<PathBuf>,
}
#[allow(dead_code)] struct EstimateArgs {
file: PathBuf,
method: String,
level: f64,
prior_alpha: f64,
prior_beta: f64,
mission_time: Option<f64>,
output: Option<PathBuf>,
}
#[allow(dead_code)] struct CalibrateArgs {
artifact: PathBuf,
event: String,
dataset: Vec<PathBuf>,
alpha: f64,
strict_alpha: f64,
min_exposure: u64,
output: Option<PathBuf>,
}
fn collect_etdl_files(paths: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
for p in paths {
let meta =
std::fs::metadata(p).map_err(|e| format!("cannot access '{}': {}", p.display(), e))?;
if meta.is_dir() {
let mut entries: Vec<PathBuf> = std::fs::read_dir(p)
.map_err(|e| format!("cannot read directory '{}': {}", p.display(), e))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "etdl"))
.collect();
entries.sort();
out.extend(entries);
} else {
out.push(p.clone());
}
}
Ok(out)
}
fn resolve_diagnostic_positions(
diagnostics: &mut [etdl_compiler::validate::Diagnostic],
content: &str,
) {
let index = etdl_parser::spanned::build_span_index(content).ok();
for d in diagnostics.iter_mut() {
if d.line.is_none() {
if let (Some(key), Some(index)) = (&d.key, &index) {
if let Some(el) = index.resolve(key) {
let span = el.key_span.unwrap_or(el.span);
d.line = Some(span.line);
d.column = Some(span.column);
d.end_line = Some(span.end_line);
d.end_column = Some(span.end_column);
}
}
}
}
}
fn append_duplicate_warnings(
diagnostics: &mut Vec<etdl_compiler::validate::Diagnostic>,
content: &str,
) {
if let Ok(duplicates) = etdl_parser::spanned::detect_duplicate_ids(content) {
for dup in duplicates {
let mut d = etdl_compiler::validate::Diagnostic::warning(
"V-001",
format!(
"duplicate {} id '{}' in tree '{}'",
dup.kind, dup.id, dup.tree
),
)
.with_position(dup.span.line, dup.span.column);
d.end_line = Some(dup.span.end_line);
d.end_column = Some(dup.span.end_column);
diagnostics.push(d);
}
}
}
#[derive(Serialize)]
struct DiagnosticJson<'a> {
code: &'a str,
severity: &'a str,
message: &'a str,
line: Option<u32>,
column: Option<u32>,
}
fn diagnostic_line(d: &etdl_compiler::validate::Diagnostic) -> String {
let level = if d.is_error() { "ERROR" } else { "WARNING" };
let position = match (d.line, d.column) {
(Some(l), Some(c)) => format!(" ({}:{})", l + 1, c + 1),
_ => String::new(),
};
format!("[{}] {}{}: {}", level, d.code, position, d.message)
}
fn print_diagnostics(flags: &CliFlags, diagnostics: &[etdl_compiler::validate::Diagnostic]) {
if flags.json {
let items: Vec<DiagnosticJson> = diagnostics
.iter()
.map(|d| DiagnosticJson {
code: &d.code,
severity: if d.is_error() { "error" } else { "warning" },
message: &d.message,
line: d.line,
column: d.column,
})
.collect();
println!("{}", serde_json::to_string(&items).unwrap_or_default());
} else {
for d in diagnostics {
if flags.quiet && !d.is_error() {
continue;
}
println!("{}", diagnostic_line(d));
}
}
}
fn cmd_compile(
flags: &CliFlags,
file: &Path,
target: &str,
out_dir: &Path,
library_path: &[PathBuf],
) -> i32 {
if target != "rust" {
eprintln!(
"error: unsupported target language '{}'; supported: rust",
target
);
return 1;
}
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
return 1;
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(registry) => registry,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let compiler = library_path
.iter()
.fold(etdl_compiler::Compiler::new(), |c, p| {
c.with_library_search_path(p.clone())
});
let base_dir = file.parent().unwrap_or(Path::new("."));
let mut result = compiler.compile_with_base(&doc, ®istry, base_dir);
append_duplicate_warnings(&mut result.diagnostics, &content);
resolve_diagnostic_positions(&mut result.diagnostics, &content);
let error_count = result.diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = result.diagnostics.iter().filter(|d| !d.is_error()).count();
print_diagnostics(flags, &result.diagnostics);
match result.rust_output {
Some(output) => {
let stem = file.file_stem().unwrap_or_default().to_string_lossy();
let out_path = out_dir.join(format!("{}.rs", stem));
if !out_dir.exists() {
if let Err(e) = std::fs::create_dir_all(out_dir) {
eprintln!("error: cannot create output directory: {}", e);
return 1;
}
}
if let Err(e) = std::fs::write(&out_path, output) {
eprintln!(
"error: cannot write generated code to {}: {}",
out_path.display(),
e
);
return 1;
}
if let Some(manifest) = &result.build_manifest {
let manifest_path = out_dir.join("etdl-build-manifest.json");
if let Ok(json) = serde_json::to_string_pretty(manifest) {
if let Err(e) = std::fs::write(&manifest_path, json) {
eprintln!(
"warning: cannot write build manifest to {}: {}",
manifest_path.display(),
e
);
} else if flags.verbose {
eprintln!(
"reliability build manifest written to {}",
manifest_path.display()
);
}
}
}
if !result.resolved_libraries.is_empty() {
let stdlib_manifest_path = out_dir.join("etdl-stdlib-manifest.json");
let payload = serde_json::json!({
"schema": etdl_compiler::stdlib::STDLIB_SCHEMA,
"libraries": result.resolved_libraries,
});
if let Ok(json) = serde_json::to_string_pretty(&payload) {
if let Err(e) = std::fs::write(&stdlib_manifest_path, json) {
eprintln!(
"warning: cannot write standard-library manifest to {}: {}",
stdlib_manifest_path.display(),
e
);
} else if flags.verbose {
eprintln!(
"standard-library manifest written to {}",
stdlib_manifest_path.display()
);
}
}
}
if !flags.quiet {
println!(
"compiled '{}' to '{}' ({} errors, {} warnings)",
file.display(),
out_path.display(),
error_count,
warning_count
);
}
0
}
None => {
if !flags.quiet {
eprintln!(
"compilation failed with {} errors and {} warnings",
error_count, warning_count
);
}
1
}
}
}
fn cmd_validate(flags: &CliFlags, paths: &[PathBuf], library_path: &[PathBuf]) -> i32 {
let files = match collect_etdl_files(paths) {
Ok(f) => f,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
if files.is_empty() {
eprintln!("error: no .etdl files found");
return 1;
}
if flags.verbose {
eprintln!("etdl: validating {} file(s)", files.len());
}
let mut worst_exit = 0;
if flags.json {
let mut results = Vec::new();
for file in &files {
let (diagnostics, ok) = validate_one(flags, file, library_path);
if !ok {
worst_exit = 1;
}
let items: Vec<DiagnosticJson> = diagnostics
.iter()
.map(|d| DiagnosticJson {
code: &d.code,
severity: if d.is_error() { "error" } else { "warning" },
message: &d.message,
line: d.line,
column: d.column,
})
.collect();
results.push(serde_json::json!({
"file": file.display().to_string(),
"valid": ok,
"diagnostics": items,
}));
}
println!("{}", serde_json::json!({ "results": results }));
return worst_exit;
}
for file in &files {
let (diagnostics, ok) = validate_one(flags, file, library_path);
let error_count = diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = diagnostics.iter().filter(|d| !d.is_error()).count();
if ok {
if !flags.quiet {
println!(
"document '{}' is valid ({} errors, {} warnings)",
file.display(),
error_count,
warning_count
);
}
} else {
worst_exit = 1;
if !flags.quiet {
eprintln!(
"document '{}' has {} validation errors",
file.display(),
error_count
);
}
}
}
worst_exit
}
fn validate_one(
flags: &CliFlags,
file: &Path,
library_path: &[PathBuf],
) -> (Vec<etdl_compiler::validate::Diagnostic>, bool) {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
return (Vec::new(), false);
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
return (Vec::new(), false);
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(registry) => registry,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
return (Vec::new(), false);
}
};
let compiler = library_path
.iter()
.fold(etdl_compiler::Compiler::new(), |c, p| {
c.with_library_search_path(p.clone())
});
let base_dir = file.parent().unwrap_or(Path::new("."));
let mut diagnostics = compiler.validate_with_base(&doc, ®istry, base_dir);
append_duplicate_warnings(&mut diagnostics, &content);
resolve_diagnostic_positions(&mut diagnostics, &content);
if !flags.json {
print_diagnostics(flags, &diagnostics);
}
let ok = !diagnostics.iter().any(|d| d.is_error());
(diagnostics, ok)
}
fn cmd_analyze(flags: &CliFlags, args: &AnalyzeArgs) -> i32 {
let file = args.file.as_path();
let dependencies = args.dependencies.as_deref();
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
return 1;
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(registry) => registry,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let compiler = args
.library_path
.iter()
.fold(etdl_compiler::Compiler::new(), |c, p| {
c.with_library_search_path(p.clone())
});
let (doc, _resolved_libs, _lib_errors) =
etdl_compiler::stdlib::expand_libraries(&doc, base_dir, &compiler.library_resolver);
let doc = &doc;
let mut diagnostics = compiler.validate_with_base(doc, ®istry, base_dir);
append_duplicate_warnings(&mut diagnostics, &content);
resolve_diagnostic_positions(&mut diagnostics, &content);
let errors: Vec<_> = diagnostics.iter().filter(|d| d.is_error()).collect();
if !errors.is_empty() {
print_diagnostics(flags, &diagnostics);
return 1;
}
#[cfg(feature = "reliability")]
let (resolved_events, _manifest) =
etdl_compiler::reliability::resolve_reliability(doc, base_dir, &mut Vec::new());
#[cfg(feature = "reliability")]
let overrides: std::collections::BTreeMap<String, f64> = resolved_events
.iter()
.map(|r| (r.override_key(), r.resolved.value))
.collect();
#[cfg(not(feature = "reliability"))]
let overrides: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
let probs = etdl_compiler::fault_tree::resolve_fault_trees_with_overrides(
doc,
&overrides,
&mut Vec::new(),
);
#[cfg(feature = "reliability")]
if dependencies.is_some()
|| args.monte_carlo.is_some()
|| args.uncertainty.is_some()
|| args.output.is_some()
{
return cmd_dependency_analysis(flags, doc, &probs, &overrides, args);
}
#[cfg(not(feature = "reliability"))]
if dependencies.is_some() || args.monte_carlo.is_some() || args.uncertainty.is_some() {
eprintln!(
"error: uncertainty / dependency-aware analysis requires the 'reliability' feature (rebuild etdl-cli with it)"
);
return 1;
}
if flags.json {
let ft_json: Vec<_> = probs
.iter()
.map(|(id, p)| serde_json::json!({ "faultTree": id, "topEventProbability": p }))
.collect();
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"eventTrees": doc.event_trees.len(),
"faultTrees": doc.fault_trees.as_ref().map(|f| f.len()).unwrap_or(0),
"faultTreeProbabilities": ft_json,
})
);
} else {
println!("document: {}", file.display());
println!("event trees: {}", doc.event_trees.len());
println!(
"fault trees: {}",
doc.fault_trees.as_ref().map(|f| f.len()).unwrap_or(0)
);
for (id, p) in &probs {
println!(" {}: topEvent probability = {:.6}", id, p);
}
}
0
}
#[cfg(feature = "reliability")]
fn build_fault_tree_spec(
doc: &etdl_parser::ast::EtlDocument,
basic_event_overrides: &std::collections::BTreeMap<String, f64>,
) -> Result<Vec<etdl_reliability::analysis::dependence::FaultTreeSpec>, String> {
use etdl_reliability::analysis::dependence::{FaultTreeSpec, GateKind, GateSpec};
let mut out = Vec::new();
let Some(fts) = &doc.fault_trees else {
return Ok(out);
};
for (ft_id, ft) in fts {
let root = ft.top_event.root_cause.clone();
let mut spec = FaultTreeSpec::new(root);
for (be_id, be) in &ft.basic_events {
let p = basic_event_overrides
.get(&etdl_compiler::fault_tree::override_key(ft_id, be_id))
.copied()
.or(be.probability)
.unwrap_or(0.0);
spec.leaves.insert(be_id.clone(), p);
}
if let Some(gates) = &ft.gates {
for (gid, g) in gates {
let kind = match g.gate_type {
etdl_parser::ast::GateType::And => GateKind::And,
etdl_parser::ast::GateType::Or => GateKind::Or,
etdl_parser::ast::GateType::Not => GateKind::Not,
etdl_parser::ast::GateType::Xor => GateKind::Xor,
etdl_parser::ast::GateType::Voting => GateKind::Voting,
etdl_parser::ast::GateType::Inhibit => GateKind::Inhibit,
etdl_parser::ast::GateType::PriorityAnd => GateKind::PriorityAnd,
};
spec.gates.insert(
gid.clone(),
GateSpec {
kind,
inputs: g.inputs.clone(),
k: g.k,
},
);
}
}
out.push(spec);
}
Ok(out)
}
#[cfg(feature = "reliability")]
fn cmd_dependency_analysis(
flags: &CliFlags,
doc: &etdl_parser::ast::EtlDocument,
_top_probs: &std::collections::BTreeMap<String, f64>,
basic_event_overrides: &std::collections::BTreeMap<String, f64>,
args: &AnalyzeArgs,
) -> i32 {
use etdl_reliability::analysis::dependence::{
analyze_with, AnalysisMetadata, AnalysisOptions, DependencyModel, InputUncertainty,
MonteCarloConfig,
};
use std::collections::BTreeMap;
let model: DependencyModel = match args.dependencies.as_deref() {
Some(dep_file) => match load_serde_file(dep_file, "dependency model") {
Ok(m) => m,
Err(code) => return code,
},
None => DependencyModel::independent(),
};
let inputs: BTreeMap<String, InputUncertainty> = match args.uncertainty.as_deref() {
Some(unc_file) => match load_serde_file(unc_file, "uncertainty inputs") {
Ok(m) => m,
Err(code) => return code,
},
None => BTreeMap::new(),
};
let specs = match build_fault_tree_spec(doc, basic_event_overrides) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
if specs.is_empty() {
eprintln!("error: no fault trees to analyze");
return 1;
}
let monte_carlo = match (args.monte_carlo, args.uncertainty.is_some()) {
(Some(0), _) => {
eprintln!("error: --monte-carlo sample count must be greater than zero");
return 1;
}
(Some(n), _) => Some(MonteCarloConfig {
samples: n,
seed: args.seed,
level: args.level,
}),
(None, true) => {
if !flags.quiet {
eprintln!(
"note: --uncertainty given without --monte-carlo; using the default \
sample count of {}",
etdl_reliability::analysis::dependence::DEFAULT_SAMPLES
);
}
Some(MonteCarloConfig {
samples: etdl_reliability::analysis::dependence::DEFAULT_SAMPLES,
seed: args.seed,
level: args.level,
})
}
(None, false) => None,
};
if args.uncertainty_ranking && monte_carlo.is_none() {
eprintln!(
"error: --uncertainty-ranking requires uncertainty propagation; supply \
--uncertainty and/or --monte-carlo"
);
return 1;
}
let mut results = Vec::new();
for spec in &specs {
let options = AnalysisOptions {
monte_carlo: monte_carlo.clone(),
inputs: inputs.clone(),
perturbation: args.perturbation,
compute_importance: args.importance,
compute_sensitivity: args.sensitivity,
compute_uncertainty_ranking: args.uncertainty_ranking,
metadata: AnalysisMetadata {
model_id: args
.file
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "fault-tree".to_string()),
..Default::default()
},
};
match analyze_with(spec, &model, &options) {
Ok(r) => results.push(r),
Err(e) => {
eprintln!(
"error: analysis of '{}' failed: {}\n\
hint: a declared dependency the analyser cannot represent is refused \
rather than evaluated under independence",
spec.top_event, e
);
return 1;
}
}
}
if let Some(path) = args.output.as_deref() {
let body = match serde_json::to_string_pretty(&results) {
Ok(b) => b,
Err(e) => {
eprintln!("error: cannot serialise analysis result: {e}");
return 1;
}
};
if let Err(e) = std::fs::write(path, body) {
eprintln!("error: cannot write '{}': {}", path.display(), e);
return 1;
}
if !flags.quiet {
eprintln!("wrote analysis result to {}", path.display());
}
}
if flags.json {
println!("{}", serde_json::to_string_pretty(&results).unwrap());
} else {
for r in &results {
print!("{}", r.render());
println!();
}
}
0
}
#[cfg(feature = "reliability")]
fn load_serde_file<T: serde::de::DeserializeOwned>(path: &Path, what: &str) -> Result<T, i32> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read {} '{}': {}", what, path.display(), e);
return Err(1);
}
};
let parsed = if path.extension().is_some_and(|e| e == "json") {
serde_json::from_str(&content).map_err(|e| e.to_string())
} else {
serde_yaml::from_str(&content).map_err(|e| e.to_string())
};
match parsed {
Ok(v) => Ok(v),
Err(e) => {
eprintln!("error: cannot parse {} '{}': {}", what, path.display(), e);
Err(1)
}
}
}
#[cfg(feature = "reliability")]
fn cmd_reliability_compare(flags: &CliFlags, before: &Path, after: &Path) -> i32 {
use etdl_reliability::analysis::dependence::{compare, AnalysisResult};
let before_result: AnalysisResult = match load_serde_file(before, "analysis result") {
Ok(r) => r,
Err(code) => return code,
};
let after_result: AnalysisResult = match load_serde_file(after, "analysis result") {
Ok(r) => r,
Err(code) => return code,
};
let comparison = compare(&before_result, &after_result);
if flags.json {
println!("{}", serde_json::to_string_pretty(&comparison).unwrap());
} else {
print!("{}", comparison.render());
}
0
}
#[cfg(feature = "discovery")]
fn cmd_discover(flags: &CliFlags, args: &DiscoverArgs) -> i32 {
use etdl_failure_discovery::config::{DiscoveryConfig, OntologyPolicy};
let path = &args.path;
let language = &args.language;
let format = &args.format;
let min_confidence = args.min_confidence;
let output = args.output.as_deref();
let exclude = &args.exclude;
let ontology_policy = &args.ontology_policy;
if language != "rust" {
eprintln!("error: language '{language}' is not implemented; only 'rust' is supported");
return 1;
}
let policy = match ontology_policy.as_str() {
"auto" => OntologyPolicy::Auto,
"conservative" => OntologyPolicy::Conservative,
"off" => OntologyPolicy::Off,
other => {
eprintln!("error: unknown ontology policy '{other}' (auto|conservative|off)");
return 1;
}
};
let config = DiscoveryConfig {
language: Some(language.to_string()),
min_confidence: min_confidence.clamp(0.0, 1.0),
ontology_policy: policy,
exclude: exclude.to_vec(),
..DiscoveryConfig::default()
};
let analyzer = etdl_failure_discovery::AnalyzerRegistry::new();
let rust = match analyzer.language("rust") {
Some(a) => a,
None => {
eprintln!("error: rust analyzer is not compiled in");
return 1;
}
};
let report = match rust.analyze_project(path, &config) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
if let Some(out) = output {
let text = match format.as_str() {
"json" => match serde_json::to_string_pretty(&report) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot serialize report: {}", e);
return 1;
}
},
"yaml" => match serde_yaml::to_string(&report) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot serialize report: {}", e);
return 1;
}
},
_ => format_report_text(&report),
};
if let Err(e) = std::fs::write(out, text) {
eprintln!("error: cannot write '{}': {}", out.display(), e);
return 1;
}
if !flags.quiet {
println!("wrote discovery report to {}", out.display());
}
return 0;
}
match format.as_str() {
"json" => println!("{}", serde_json::to_string_pretty(&report).unwrap()),
"yaml" => println!("{}", serde_yaml::to_string(&report).unwrap()),
_ => print!("{}", format_report_text(&report)),
}
0
}
#[cfg(feature = "discovery")]
fn format_report_text(report: &etdl_failure_discovery::DiscoveryReport) -> String {
let mut out = String::new();
out.push_str("Failure Discovery Report\n");
out.push_str("========================\n");
out.push_str(&format!("Schema: {}\n", report.schema));
out.push_str(&format!(
"Analyzer: {} v{}\n",
report.analyzer.name, report.analyzer.version
));
out.push_str(&format!("Language: {}\n", report.analyzer.language));
out.push_str(&format!(
"Source: {}\n",
report.source.path.display()
));
out.push_str(&format!("Files: {}\n", report.source.file_count));
out.push_str(&format!("Content hash: {}\n", report.source.content_hash));
out.push('\n');
let stats = &report.statistics;
out.push_str(&format!("Candidates: {}\n", stats.total_candidates));
out.push_str(&format!(
"High confidence (>=0.8): {}\n",
stats.high_confidence
));
out.push_str(&format!(
"Potential panic: {}\n",
stats.potential_panic
));
out.push_str(&format!("Mapped to ontology: {}\n", stats.mapped));
out.push_str(&format!("Unmapped (proposed): {}\n", stats.unmapped));
for (class, count) in &stats.by_classification {
out.push_str(&format!(" - {class}: {count}\n"));
}
out.push('\n');
for c in &report.candidates {
let onto = match &c.ontology.canonical_id {
Some(id) => id.clone(),
None => c
.ontology
.proposed_concept
.clone()
.unwrap_or_else(|| "(unmapped)".to_string()),
};
let first_evidence = c
.evidence
.first()
.map(|e| e.detail.clone())
.unwrap_or_default();
out.push_str(&format!(
"{} [{}] {}:{} -> {} (conf={:.2}, {:?})\n",
c.id,
c.classification.label(),
c.location.file.display(),
c.location.line,
onto,
c.confidence,
c.ontology.quality,
));
out.push_str(&format!(" evidence: {}\n", first_evidence));
if let Some(line) = c.evidence.first().and_then(|e| e.line_text.clone()) {
out.push_str(&format!(" source: {}\n", line));
}
}
out
}
#[cfg(feature = "reliability")]
fn cmd_reliability_resolve(flags: &CliFlags, file: &Path) -> i32 {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
return 1;
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let compiler = etdl_compiler::Compiler::new();
let mut diagnostics = compiler.validate_with_base(&doc, ®istry, base_dir);
if diagnostics.iter().any(|d| d.is_error()) {
print_diagnostics(flags, &diagnostics);
return 1;
}
let (resolved, manifest) =
etdl_compiler::reliability::resolve_reliability(&doc, base_dir, &mut diagnostics);
if flags.json {
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"resolved": resolved.iter().map(|r| serde_json::json!({
"basic_event": r.basic_event,
"value": r.resolved.value,
"artifact": r.resolved.artifact_id,
"artifact_version": r.resolved.artifact_version,
"estimate": r.resolved.estimate_id,
})).collect::<Vec<_>>(),
"manifest": manifest,
})
);
return 0;
}
if resolved.is_empty() {
println!("no external probability sources resolved");
}
for r in &resolved {
println!(
"basic event '{}': value={} from artifact '{}' v{} estimate '{}'",
r.basic_event,
r.resolved.value,
r.resolved.artifact_id,
r.resolved.artifact_version.as_deref().unwrap_or("?"),
r.resolved.estimate_id
);
}
0
}
#[cfg(feature = "reliability")]
fn cmd_reliability_validate(flags: &CliFlags, file: &Path) -> i32 {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
return 1;
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let compiler = etdl_compiler::Compiler::new();
let diagnostics = compiler.validate_with_base(&doc, ®istry, base_dir);
let mut issues = Vec::new();
if let Some(ext) = doc.extensions.get("x-reliability") {
if let Some(obj) = ext.as_mapping() {
if let Some(sources) = obj.get(serde_yaml::Value::String("sources".into())) {
if let Some(arr) = sources.as_sequence() {
for src in arr {
if let Some(map) = src.as_mapping() {
let id = map
.get(serde_yaml::Value::String("id".into()))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let file = map
.get(serde_yaml::Value::String("file".into()))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if file.is_empty() {
continue;
}
let path = if file.split('/').any(|seg| seg == "..") {
base_dir.join(&file)
} else {
let p = Path::new(&file);
if p.is_absolute() {
p.to_path_buf()
} else {
base_dir.join(p)
}
};
match etdl_compiler::reliability::load_artifact_for_cli(&path) {
Ok(artifact) => {
let found =
etdl_reliability_core::validation::validate_artifact_issues(
&artifact,
);
for issue in &found {
issues
.push(format!("source '{}' ({}): {}", id, file, issue));
}
}
Err(e) => issues.push(format!("source '{}' ({}): {}", id, file, e)),
}
}
}
}
}
}
}
let has_errors = diagnostics.iter().any(|d| d.is_error()) || !issues.is_empty();
if flags.json {
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"diagnostics": diagnostics.iter().map(|d| serde_json::json!({
"code": d.code,
"severity": format!("{:?}", d.severity),
"message": d.message,
})).collect::<Vec<_>>(),
"artifact_issues": issues,
"valid": !has_errors,
})
);
return if has_errors { 1 } else { 0 };
}
print_diagnostics(flags, &diagnostics);
if issues.is_empty() {
println!("reliability artifacts: OK");
} else {
for i in &issues {
println!("issue: {}", i);
}
}
if has_errors {
1
} else {
0
}
}
#[cfg(feature = "reliability")]
fn cmd_reliability_inspect(flags: &CliFlags, file: &Path) -> i32 {
let artifact = match etdl_compiler::reliability::load_artifact_for_cli(file) {
Ok(a) => a,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let issues = etdl_reliability_core::validation::validate_artifact_issues(&artifact);
if flags.json {
let estimates: Vec<_> = artifact
.estimates
.iter()
.map(|(key, est)| {
serde_json::json!({
"key": key,
"event": est.event,
"state": format!("{:?}", est.state),
"value": est.value,
"metric": format!("{:?}", est.metric),
"time_basis": est.time_basis.map(|t| t.to_string()),
"conditions": est.conditions,
"has_uncertainty": est.uncertainty.is_some(),
"has_provenance": est.provenance.is_some(),
})
})
.collect();
println!(
"{}",
serde_json::json!({
"schema": artifact.schema,
"id": artifact.id,
"version": artifact.version,
"estimate_count": artifact.estimates.len(),
"estimates": estimates,
"issues": issues.iter().map(|i| i.to_string()).collect::<Vec<_>>(),
"valid": issues.is_empty(),
})
);
return if issues.is_empty() { 0 } else { 1 };
}
println!("artifact: {}", artifact.id);
println!("schema: {}", artifact.schema);
println!(
"version: {}",
artifact.version.as_deref().unwrap_or("(missing)")
);
println!("estimates: {}", artifact.estimates.len());
for (key, est) in &artifact.estimates {
let value = est
.value
.map(|v| v.to_string())
.unwrap_or_else(|| "(unknown)".to_string());
let tb = est
.time_basis
.map(|t| format!(" @{}", t))
.unwrap_or_default();
println!(" {} = {} ({:?}{})", key, value, est.metric, tb);
}
if issues.is_empty() {
println!("validation: OK");
0
} else {
for i in &issues {
println!("validation issue: {}", i);
}
1
}
}
#[cfg(feature = "reliability")]
fn cmd_reliability_estimate(flags: &CliFlags, args: &EstimateArgs) -> i32 {
use etdl_reliability::analysis::{builtin_estimators, EstimationConfig};
use etdl_reliability::observations::ObservationSet;
use etdl_reliability_core::artifact::ReliabilityArtifact;
let file = &args.file;
let method = &args.method;
let output = args.output.as_deref();
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!(
"error: cannot read observations '{}': {}",
file.display(),
e
);
return 1;
}
};
let set: ObservationSet = match serde_yaml::from_str(&content) {
Ok(s) => s,
Err(e) => {
eprintln!(
"error: cannot parse observations '{}': {}",
file.display(),
e
);
return 1;
}
};
if let Err(e) = set.validate() {
eprintln!("error: invalid observations: {}", e);
return 1;
}
let estimator = builtin_estimators()
.into_iter()
.find(|e| match method.as_str() {
"empirical" => e.name() == "empirical/binomial",
"beta-binomial" => e.name() == "bayesian/beta-binomial",
"exponential" => e.name() == "exponential/constant-rate",
_ => false,
});
let estimator = match estimator {
Some(e) => e,
None => {
eprintln!(
"error: unknown estimator '{}' (empirical | beta-binomial | exponential)",
method
);
return 1;
}
};
let config = EstimationConfig {
level: args.level,
prior_alpha: args.prior_alpha,
prior_beta: args.prior_beta,
mission_time: args.mission_time,
metric: etdl_reliability_core::probability::ProbabilityMetric::Probability,
};
let mut artifact = ReliabilityArtifact::new("etdl-estimate");
artifact.version = Some("1.0.0".to_string());
let mut results = Vec::new();
for obs in &set.observations {
match estimator.estimate(obs, &config) {
Ok(est) => {
if let Err(e) = artifact.add(est.clone()) {
eprintln!(
"error: cannot store estimate for '{}': {}",
obs.failure_mode, e
);
return 1;
}
results.push(est);
}
Err(e) => {
eprintln!("error: estimation failed for '{}': {}", obs.failure_mode, e);
return 1;
}
}
}
if let Some(out) = output {
let text = if out.extension().is_some_and(|e| e == "json") {
serde_json::to_string_pretty(&artifact).unwrap()
} else {
serde_yaml::to_string(&artifact).unwrap()
};
if let Err(e) = std::fs::write(out, text) {
eprintln!("error: cannot write '{}': {}", out.display(), e);
return 1;
}
if !flags.quiet {
println!("wrote reliability artifact to {}", out.display());
}
}
if flags.json {
println!(
"{}",
serde_json::json!({
"estimator": estimator.name(),
"version": estimator.version(),
"assumptions": estimator.assumptions(),
"estimates": results,
})
);
} else {
println!("estimator: {} v{}", estimator.name(), estimator.version());
for a in estimator.assumptions() {
println!(" assumption: {}", a);
}
for est in &results {
let value = est
.value
.map(|v| v.to_string())
.unwrap_or_else(|| "(unknown)".to_string());
println!(
" {} = {} ({})",
est.event,
value,
est.method.clone().unwrap_or_default()
);
if let Some(u) = &est.uncertainty {
println!(" uncertainty: {:?}", u);
}
}
}
0
}
#[cfg(feature = "reliability")]
fn cmd_reliability_trace(flags: &CliFlags, file: &Path, estimate_id: &str) -> i32 {
let artifact = match etdl_compiler::reliability::load_artifact_for_cli(file) {
Ok(a) => a,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let estimate = match artifact.get(estimate_id) {
Some(e) => e,
None => {
eprintln!("error: no estimate '{}' in artifact", estimate_id);
return 1;
}
};
let trace = etdl_reliability::trace_from_estimate(
estimate,
Some(&artifact.id),
artifact.version.as_deref(),
);
if flags.json {
println!("{}", serde_json::to_string_pretty(&trace).unwrap());
} else {
println!("Trace for '{}':", estimate_id);
print!("{}", trace.render());
}
0
}
fn cmd_library_list(flags: &CliFlags) -> i32 {
let libs = etdl_compiler::stdlib::list_builtin();
let mut had_error = false;
if flags.json {
let items: Vec<serde_json::Value> = libs
.iter()
.map(|r| match r {
Ok(lib) => serde_json::json!({
"name": lib.name,
"version": lib.version,
"kind": lib.kind.label(),
"description": lib.description,
"basic_events": lib.basic_events.keys().collect::<Vec<_>>(),
"gates": lib.gates.keys().collect::<Vec<_>>(),
}),
Err(e) => serde_json::json!({ "error": e.to_string() }),
})
.collect();
println!(
"{}",
serde_json::json!({
"schema": etdl_compiler::stdlib::STDLIB_SCHEMA,
"libraries": items,
})
);
} else {
println!("ETDL Standard Library ({})", etdl_compiler::stdlib::STDLIB_SCHEMA);
for r in &libs {
match r {
Ok(lib) => {
println!(" {} v{} (built-in)", lib.name, lib.version);
if let Some(d) = &lib.description {
println!(" {}", d);
}
if !lib.basic_events.is_empty() {
println!(
" basic events: {}",
lib.basic_events.keys().cloned().collect::<Vec<_>>().join(", ")
);
}
if !lib.gates.is_empty() {
println!(
" gates: {}",
lib.gates.keys().cloned().collect::<Vec<_>>().join(", ")
);
}
}
Err(e) => {
eprintln!(" error: {}", e);
had_error = true;
}
}
}
}
if had_error {
1
} else {
0
}
}
fn cmd_library_resolve(flags: &CliFlags, file: &Path, library_path: &[PathBuf]) -> i32 {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
return 1;
}
};
let doc = match etdl_parser::parse_document(&content) {
Ok(doc) => doc,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let base_dir = file.parent().unwrap_or(Path::new("."));
let resolver = library_path
.iter()
.fold(etdl_compiler::stdlib::LibraryResolver::new(), |r, p| {
r.with_search_path(p.clone())
});
let (_expanded, resolved, errors) = etdl_compiler::stdlib::expand_libraries(&doc, base_dir, &resolver);
if flags.json {
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"resolved": resolved.iter().map(|l| l.provenance()).collect::<Vec<_>>(),
"errors": errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
})
);
} else {
if doc.libraries.is_empty() {
println!("document declares no libraries");
}
for import in &doc.libraries {
match resolved.iter().find(|l| l.name == import.name) {
Some(lib) => println!(
" {} requested {} -> resolved {} ({}){}",
import.name,
import.version,
lib.version,
lib.kind.label(),
if import.required { "" } else { " [optional]" }
),
None => println!(
" {} requested {} -> UNRESOLVED{}",
import.name,
import.version,
if import.required { "" } else { " [optional]" }
),
}
}
for e in &errors {
println!(" error: {}", e);
}
}
if errors.is_empty() {
0
} else {
1
}
}
fn parse_document_or_exit(file: &Path) -> Result<etdl_parser::ast::EtlDocument, i32> {
let content = std::fs::read_to_string(file).map_err(|e| {
eprintln!("error: cannot read file '{}': {}", file.display(), e);
1
})?;
etdl_parser::parse_document(&content).map_err(|e| {
eprintln!("error: {}", e);
1
})
}
fn cmd_tree_validate(flags: &CliFlags, file: &Path) -> i32 {
let doc = match parse_document_or_exit(file) {
Ok(d) => d,
Err(code) => return code,
};
let (trees, diagnostics) = etdl_compiler::tree_event::parse_and_validate_trees(&doc);
if flags.json {
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"valid": diagnostics.is_empty(),
"trees": trees.iter().map(|t| &t.id).collect::<Vec<_>>(),
"diagnostics": diagnostics.iter().map(|d| serde_json::json!({
"code": d.code,
"severity": if d.is_error() { "error" } else { "warning" },
"message": d.message,
})).collect::<Vec<_>>(),
})
);
} else if diagnostics.is_empty() {
if trees.is_empty() {
println!("document declares no trees under x-tree-event (or the supplement is not declared)");
} else {
println!(
"{} tree(s) valid: {}",
trees.len(),
trees.iter().map(|t| t.id.as_str()).collect::<Vec<_>>().join(", ")
);
}
} else {
for d in &diagnostics {
println!("[{}] {}", d.code, d.message);
}
}
if diagnostics.iter().any(|d| d.is_error()) {
1
} else {
0
}
}
fn cmd_tree_inspect(flags: &CliFlags, file: &Path) -> i32 {
let doc = match parse_document_or_exit(file) {
Ok(d) => d,
Err(code) => return code,
};
let (trees, diagnostics) = etdl_compiler::tree_event::parse_and_validate_trees(&doc);
if flags.json {
let items: Vec<_> = trees
.iter()
.map(|t| {
serde_json::json!({
"id": t.id,
"version": t.version,
"schema": t.schema,
"root": t.root,
"node_count": t.nodes.len(),
"leaves": t.leaves(),
"preorder": t.preorder(),
})
})
.collect();
println!(
"{}",
serde_json::json!({
"document": file.display().to_string(),
"trees": items,
"diagnostics": diagnostics.iter().map(|d| d.message.clone()).collect::<Vec<_>>(),
})
);
} else {
if trees.is_empty() && diagnostics.is_empty() {
println!("document declares no trees under x-tree-event (or the supplement is not declared)");
}
for t in &trees {
println!("tree: {} v{} ({})", t.id, t.version, t.schema);
println!(" root: {}", t.root);
println!(" nodes: {}", t.nodes.len());
println!(" leaves: {}", t.leaves().join(", "));
println!(" preorder: {}", t.preorder().join(" -> "));
}
for d in &diagnostics {
println!("issue: [{}] {}", d.code, d.message);
}
}
if diagnostics.iter().any(|d| d.is_error()) {
1
} else {
0
}
}
#[cfg(feature = "reliability")]
fn cmd_reliability_calibrate(flags: &CliFlags, args: &CalibrateArgs) -> i32 {
use etdl_reliability::calibration::{calibrate, CalibrationConfig};
use etdl_reliability::dataset::{aggregate_across, ObservationDataset};
let artifact = match etdl_compiler::reliability::load_artifact_for_cli(&args.artifact) {
Ok(a) => a,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
let mut datasets: Vec<ObservationDataset> = Vec::new();
for path in &args.dataset {
let ds: ObservationDataset = match load_serde_file(path, "observation dataset") {
Ok(d) => d,
Err(code) => return code,
};
if let Err(e) = ds.validate() {
eprintln!(
"error: dataset '{}' ({}) is invalid: {}",
ds.id,
path.display(),
e
);
return 1;
}
datasets.push(ds);
}
let refs: Vec<&ObservationDataset> = datasets.iter().collect();
let aggregated = match aggregate_across(&refs, &args.event) {
Ok(a) => a,
Err(e) => {
eprintln!("error: cannot aggregate observations for '{}': {}", args.event, e);
return 1;
}
};
if args.alpha <= 0.0 || args.alpha >= 1.0 {
eprintln!("error: --alpha must be in (0, 1), got {}", args.alpha);
return 1;
}
if args.strict_alpha <= 0.0 || args.strict_alpha >= args.alpha {
eprintln!(
"error: --strict-alpha must be in (0, {}), got {}",
args.alpha, args.strict_alpha
);
return 1;
}
let config = CalibrationConfig {
alpha: args.alpha,
strict_alpha: args.strict_alpha,
min_exposure: args.min_exposure,
};
let result = match calibrate(
&artifact,
&args.event,
&aggregated.observation,
aggregated.provenance.source_datasets.clone(),
&config,
) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {}", e);
return 1;
}
};
if let Some(path) = args.output.as_deref() {
match serde_json::to_string_pretty(&result) {
Ok(body) => {
if let Err(e) = std::fs::write(path, body) {
eprintln!("error: cannot write '{}': {}", path.display(), e);
return 1;
}
if !flags.quiet {
eprintln!("wrote calibration result to {}", path.display());
}
}
Err(e) => {
eprintln!("error: cannot serialise calibration result: {e}");
return 1;
}
}
}
if flags.json {
println!("{}", serde_json::to_string_pretty(&result).unwrap());
} else {
print!("{}", result.render());
if result.is_drift() {
println!(
"note: this indicates model drift under the configured significance level; \
review before publishing a new estimate. Nothing has been changed \
automatically."
);
}
}
0
}
#[allow(dead_code)] fn capability_unavailable(capability: &str, how: &str) {
eprintln!(
"{} support is not enabled in this build.\n {}",
capability, how
);
}
#[cfg(feature = "reliability")]
fn predictive_reliability_schema(_analysis: bool) -> &'static str {
etdl_reliability::predictive::PREDICTIVE_SCHEMA
}
#[cfg(not(feature = "reliability"))]
fn predictive_reliability_schema(_analysis: bool) -> &'static str {
"unavailable"
}
#[cfg(feature = "reliability")]
fn reliability_artifact_schema() -> Option<&'static str> {
Some(etdl_reliability_core::artifact::ARTIFACT_SCHEMA)
}
#[cfg(not(feature = "reliability"))]
fn reliability_artifact_schema() -> Option<&'static str> {
None
}
fn cmd_conformance_status(flags: &CliFlags) -> i32 {
let reliability = cfg!(feature = "reliability");
let areas = etdl_conformance::report::area_statuses(reliability);
if flags.json {
println!("{}", serde_json::to_string_pretty(&areas).unwrap());
} else {
println!("ETDL Conformance Status ({})", etdl_conformance::CONFORMANCE_SUITE_VERSION);
for area in &areas {
println!(" [{}] {:<45} {}", area.status, area.area, area.detail);
}
}
0
}
fn cmd_conformance_manifest(flags: &CliFlags) -> i32 {
let reliability = cfg!(feature = "reliability");
let manifest = etdl_conformance::manifest::ConformanceManifest::build(
env!("CARGO_PKG_VERSION"),
reliability,
etdl_tree_core::TREE_SCHEMA,
etdl_probability_core::STD_PROBABILITY_SCHEMA,
etdl_compiler::stdlib::STDLIB_SCHEMA,
predictive_reliability_schema(reliability),
reliability_artifact_schema(),
);
if flags.json {
println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
} else {
println!("ETDL language version: {}", manifest.etdl_language_version);
println!("Implementation version: {}", manifest.implementation_version);
println!("Conformance suite version: {}", manifest.conformance_suite_version);
println!("Supplements:");
for s in &manifest.supported_supplements {
println!(
" {} ({}) — {}",
s.id,
s.version,
if s.available { "available" } else { "unavailable" }
);
}
println!("Libraries: {}", manifest.supported_libraries.join(", "));
println!("Targets: {}", manifest.supported_targets.join(", "));
println!("Artifact schemas: {}", manifest.supported_artifact_schemas.join(", "));
}
0
}
fn cmd_capabilities(flags: &CliFlags) -> i32 {
let reliability = cfg!(feature = "reliability");
let discovery = cfg!(feature = "discovery");
let ontology = discovery;
let analysis = reliability;
let registry = etdl_compiler::extension::builtin_registry();
let caps = serde_json::json!({
"core": true,
"reliability": {
"available": reliability,
"kind": if reliability { "built-in" } else { "unavailable" },
},
"reliability_analysis": analysis,
"statistical_estimation": analysis,
"uncertainty_analysis": analysis,
"monte_carlo": {
"available": analysis,
"sampler": if analysis { "xorshift64star" } else { "unavailable" },
"sampler_version": if analysis { "1" } else { "unavailable" },
"method": if analysis { "monte-carlo-propagation" } else { "unavailable" },
},
"importance": {
"available": analysis,
"measures": if analysis {
serde_json::json!([
"birnbaum",
"fussell-vesely",
"criticality",
"risk-achievement-worth",
"risk-reduction-worth"
])
} else {
serde_json::json!([])
},
},
"sensitivity": {
"available": analysis,
"method": if analysis {
"finite-perturbation/absolute/two-sided"
} else {
"unavailable"
},
},
"uncertainty_ranking": analysis,
"analysis_comparison": analysis,
"standard_library": {
"available": true,
"schema": etdl_compiler::stdlib::STDLIB_SCHEMA,
"builtin_libraries": etdl_compiler::stdlib::LibraryResolver::builtin_names(),
},
"std_probability": {
"available": true,
"schema": etdl_probability_core::STD_PROBABILITY_SCHEMA,
"kind": "built-in",
"distributions": ["bernoulli", "binomial", "beta", "exponential", "normal"],
"sampling": "unavailable (deterministic math only; see docs/reference/standard-probability-library.md)",
},
"tree_event": {
"available": true,
"schema": etdl_tree_core::TREE_SCHEMA,
"kind": "built-in",
"gates": ["AND", "OR", "NOT", "XOR", "K_OF_N"],
"structure": "tree (strict single-parent), not a DAG",
},
"runtime_feedback": {
"available": analysis,
"observation_dataset": analysis,
"calibration": {
"available": analysis,
"method": if analysis { "binomial-two-sided-exact" } else { "unavailable" },
},
},
"predictive_reliability": {
"available": analysis,
"schema": predictive_reliability_schema(analysis),
"kind": if analysis { "built-in" } else { "unavailable" },
"models": if analysis { serde_json::json!(["exponential", "weibull"]) } else { serde_json::json!([]) },
"quantities": if analysis {
serde_json::json!(["survival", "reliability", "failure-probability", "hazard", "cumulative-hazard", "density"])
} else {
serde_json::json!([])
},
"sampling": "unavailable (deterministic closed-form math only; see docs/reference/predictive-reliability-supplement.md)",
"censored_data_fitting": "unavailable (censored observations can be represented, not fit; see docs)",
},
"correlated_parameter_uncertainty": false,
"conditional_probability_evaluation": false,
"failure_discovery": discovery,
"ontology": ontology,
"extensions": registry
.list()
.iter()
.map(|id| {
let ext = registry.lookup(id).expect("listed");
serde_json::json!({
"id": ext.id(),
"version": ext.version(),
})
})
.collect::<Vec<_>>(),
"compiler_version": env!("CARGO_PKG_VERSION"),
});
if flags.json {
println!("{}", caps);
} else {
println!("etdl {}", env!("CARGO_PKG_VERSION"));
println!("Core: yes");
println!(
"Standard library: available ({}) — built-in: {}",
etdl_compiler::stdlib::STDLIB_SCHEMA,
etdl_compiler::stdlib::LibraryResolver::builtin_names().join(", ")
);
println!(
"std.probability: available ({}) — distributions: bernoulli, binomial, beta, \
exponential, normal; sampling: unavailable (deterministic math only)",
etdl_probability_core::STD_PROBABILITY_SCHEMA
);
println!(
"Generic Tree Event Supplement: available ({}) — gates: AND, OR, NOT, XOR, K_OF_N; \
tree (not DAG)",
etdl_tree_core::TREE_SCHEMA
);
if reliability {
println!("Reliability: built-in");
println!("Reliability Analysis: available");
println!("Statistical estimation: available");
println!("Uncertainty analysis: available");
println!("Monte Carlo: available (xorshift64star/1, monte-carlo-propagation/1)");
println!(
"Importance: available (birnbaum, fussell-vesely, criticality, \
risk-achievement-worth, risk-reduction-worth)"
);
println!("Sensitivity: available (finite-perturbation/absolute/two-sided)");
println!("Uncertainty ranking: available");
println!("Analysis comparison: available");
println!("Runtime feedback / calibration: available (binomial-two-sided-exact)");
} else {
println!("Reliability: unavailable (rebuild with 'reliability')");
println!("Reliability Analysis: unavailable (rebuild with 'reliability')");
println!("Statistical estimation: unavailable (rebuild with 'reliability')");
println!("Uncertainty analysis: unavailable (rebuild with 'reliability')");
println!("Monte Carlo: unavailable (rebuild with 'reliability')");
println!("Importance: unavailable (rebuild with 'reliability')");
println!("Sensitivity: unavailable (rebuild with 'reliability')");
println!("Runtime feedback / calibration: unavailable (rebuild with 'reliability')");
}
println!("Correlated parameter uncertainty: unsupported");
println!("Conditional probability evaluation: unsupported");
if discovery {
println!("Failure Discovery: available");
println!("Ontology: available");
} else {
println!("Failure Discovery: unavailable (rebuild with 'discovery')");
println!("Ontology: unavailable (rebuild with 'discovery')");
}
println!("Registered extensions:");
for id in registry.list() {
let ext = registry.lookup(id).expect("listed");
println!(" {} @{}", ext.id(), ext.version());
}
}
0
}