use clap::{Parser, Subcommand};
use nusy_codegraph::ingest_pipeline::{ingest_workspace, verify_graph, write_graph_parquet};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "nusy-codegraph-ingest",
about = "Ingest NuSy Rust crates into a CodeGraph and verify coherence",
version
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Ingest {
#[arg(long, conflicts_with = "crate_dir")]
workspace: Option<PathBuf>,
#[arg(long = "crate", conflicts_with = "workspace")]
crate_dir: Option<PathBuf>,
#[arg(long)]
output: PathBuf,
#[arg(long, default_value_t = false)]
verbose: bool,
},
Verify {
#[arg(long)]
graph: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Ingest {
workspace,
crate_dir,
output,
verbose,
} => {
run_ingest(workspace, crate_dir, output, verbose);
}
Commands::Verify { graph } => {
run_verify(graph);
}
}
}
fn run_ingest(
workspace: Option<PathBuf>,
crate_dir: Option<PathBuf>,
output: PathBuf,
verbose: bool,
) {
let root = match (workspace, crate_dir) {
(Some(ws), None) => ws,
(None, Some(cr)) => cr,
_ => {
eprintln!("error: must specify --workspace or --crate");
std::process::exit(1);
}
};
if !root.exists() {
eprintln!("error: path does not exist: {}", root.display());
std::process::exit(1);
}
eprintln!("nusy-codegraph-ingest: ingesting {}", root.display());
let result = ingest_workspace(&root);
eprintln!("{}", result.summary());
if verbose {
let mut crate_names: Vec<&str> =
result.crates.keys().map(|s: &String| s.as_str()).collect();
crate_names.sort();
for name in crate_names {
let cr = &result.crates[name];
eprintln!(
" {name}: {} nodes, {} edges, {} errors",
cr.nodes.len(),
cr.edges.len(),
cr.errors.len()
);
}
}
let nodes_batch: arrow::array::RecordBatch = match result.merged_nodes_batch() {
Ok(b) => b,
Err(e) => {
eprintln!("error: failed to build CodeNodes batch: {e}");
std::process::exit(1);
}
};
let edges_batch: arrow::array::RecordBatch = match result.merged_edges_batch() {
Ok(b) => b,
Err(e) => {
eprintln!("error: failed to build CodeEdges batch: {e}");
std::process::exit(1);
}
};
let violations = verify_graph(&nodes_batch, &edges_batch);
eprint!("{}", violations.report());
if let Err(e) = write_graph_parquet(&nodes_batch, &edges_batch, &output) {
eprintln!("error: failed to write Parquet: {e}");
std::process::exit(1);
}
eprintln!(
"nusy-codegraph-ingest: wrote {nodes} nodes + {edges} edges to {out}",
nodes = nodes_batch.num_rows(),
edges = edges_batch.num_rows(),
out = output.display()
);
if violations.is_clean() {
eprintln!("Graph coherence: PASS");
} else {
eprintln!("Graph coherence: warnings (see above)");
}
}
fn run_verify(graph_dir: PathBuf) {
let nodes_path = graph_dir.join("nodes.parquet");
let edges_path = graph_dir.join("edges.parquet");
if !nodes_path.exists() {
eprintln!("error: nodes.parquet not found in {}", graph_dir.display());
std::process::exit(1);
}
if !edges_path.exists() {
eprintln!("error: edges.parquet not found in {}", graph_dir.display());
std::process::exit(1);
}
let nodes_batch = read_parquet_first_batch(&nodes_path);
let edges_batch = read_parquet_first_batch(&edges_path);
eprintln!(
"Loaded: {} nodes, {} edges",
nodes_batch.num_rows(),
edges_batch.num_rows()
);
let violations = verify_graph(&nodes_batch, &edges_batch);
print!("{}", violations.report());
if !violations.is_clean() {
std::process::exit(1);
}
}
fn read_parquet_first_batch(path: &std::path::Path) -> arrow::array::RecordBatch {
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::fs::File;
let file = File::open(path).unwrap_or_else(|e| {
eprintln!("error: cannot open {}: {e}", path.display());
std::process::exit(1);
});
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap_or_else(|e| {
eprintln!("error: cannot read Parquet {}: {e}", path.display());
std::process::exit(1);
});
let mut reader = builder.build().unwrap_or_else(|e| {
eprintln!("error: cannot build reader {}: {e}", path.display());
std::process::exit(1);
});
let mut batches: Vec<arrow::array::RecordBatch> = Vec::new();
for batch in &mut reader {
match batch {
Ok(b) => batches.push(b),
Err(e) => {
eprintln!("warning: batch read error: {e}");
}
}
}
if batches.is_empty() {
eprintln!("error: no data in {}", path.display());
std::process::exit(1);
}
arrow::compute::concat_batches(&batches[0].schema(), &batches).unwrap_or_else(|e| {
eprintln!("error: concat batches: {e}");
std::process::exit(1);
})
}