use afterautism_adapter::Adapter;
use afterautism_adapter_markdown::MarkdownAdapter;
use afterautism_adapter_records::RecordsAdapter;
use afterautism_ingest::IngestCoordinator;
use afterautism_query::QueryExpr;
use afterautism_storage::{Corpus, StagingCorpus};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "afterautism", version, about = "AfterAutism engine CLI")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Create { path: PathBuf },
Ingest {
#[arg(long)]
corpus: PathBuf,
#[arg(required = true)]
files: Vec<PathBuf>,
},
Query {
#[arg(long)]
corpus: PathBuf,
#[arg(long, default_value_t = 20)]
limit: usize,
query: String,
},
Export {
#[arg(long)]
corpus: PathBuf,
},
Backup {
#[arg(long)]
corpus: PathBuf,
dest: PathBuf,
},
Restore {
#[arg(long)]
corpus: PathBuf,
src: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Command::Create { path } => cmd_create(&path),
Command::Ingest { corpus, files } => cmd_ingest(&corpus, &files),
Command::Query {
corpus,
limit,
query,
} => cmd_query(&corpus, &query, limit),
Command::Export { corpus } => cmd_export(&corpus),
Command::Backup { corpus, dest } => cmd_backup(&corpus, &dest),
Command::Restore { corpus, src } => cmd_restore(&corpus, &src),
};
if let Err(e) = result {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn cmd_create(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let staging = StagingCorpus::create(path)?;
let live = PathBuf::from(format!("{}.live", path.display()));
staging.commit_to(&live)?;
println!("created corpus: {}", path.display());
Ok(())
}
fn cmd_ingest(corpus: &Path, files: &[PathBuf]) -> Result<(), Box<dyn std::error::Error>> {
let mut corpus = Corpus::open(corpus)?;
let coord = IngestCoordinator::new();
let mut total_nodes = 0usize;
let mut total_edges = 0usize;
for file in files {
let adapter = pick_adapter(file)?;
let start_id = namespace_for(adapter.id());
let source = afterautism_ingest::Source::with_meta(
file.display().to_string(),
afterautism_ingest::SourceMeta::default(),
);
let (report, batch) = coord.ingest_once_with_batch(&*adapter, &source, None, "cli")?;
let shifted = shift_batch(batch, start_id);
corpus.write_batch(&shifted, "cli")?;
total_nodes += report.nodes;
total_edges += report.edges;
println!(
"ingested {}: {} nodes, {} edges",
file.display(),
report.nodes,
report.edges
);
}
println!("total: {total_nodes} nodes, {total_edges} edges in {corpus:?}");
Ok(())
}
fn cmd_query(corpus: &Path, query: &str, limit: usize) -> Result<(), Box<dyn std::error::Error>> {
let corpus = Corpus::open(corpus)?;
let expr = QueryExpr::parse(query).map_err(|e| e.to_string())?;
let opts = afterautism_query::ExecOptions {
limit: Some(limit),
after: None,
};
let result = afterautism_query::exec::execute(&corpus, &expr, &opts)?;
println!(
"matched {} total (showing {})",
result.total.unwrap_or(0),
result.node_ids.len()
);
for id in &result.node_ids {
if let Some(node) = corpus.get_node(*id)? {
println!(" [{:?}] {}", id, node.label);
}
}
Ok(())
}
fn cmd_export(corpus: &Path) -> Result<(), Box<dyn std::error::Error>> {
let corpus = Corpus::open(corpus)?;
let mut cursor = None;
loop {
let (page, next) = corpus.page_nodes(cursor, 500)?;
for node in &page {
let rec = serde_json::json!({
"id": node.id.to_raw(),
"label": node.label,
"kind": node.kind.as_str(),
});
println!("{}", serde_json::to_string(&rec)?);
}
match next {
Some(n) => cursor = Some(n),
None => break,
}
}
Ok(())
}
fn cmd_backup(corpus: &Path, dest: &Path) -> Result<(), Box<dyn std::error::Error>> {
let corpus = Corpus::open(corpus)?;
corpus.backup_to(dest)?;
println!("backed up to {}", dest.display());
Ok(())
}
fn cmd_restore(corpus: &Path, src: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut corpus = Corpus::open(corpus)?;
corpus.restore_from(src)?;
println!("restored from {}", src.display());
Ok(())
}
fn namespace_for(adapter_id: afterautism_core::AdapterId) -> u64 {
(adapter_id.to_raw() & 0xFFFF) << 20
}
fn shift_batch(
batch: afterautism_adapter::IngestBatch,
base: u64,
) -> afterautism_adapter::IngestBatch {
let nodes = batch
.nodes
.into_iter()
.map(|mut n| {
n.id = afterautism_core::NodeId::from_raw(n.id.to_raw() + base);
n
})
.collect();
let edges = batch
.edges
.into_iter()
.map(|mut e| {
e.from = afterautism_core::NodeId::from_raw(e.from.to_raw() + base);
e.to = afterautism_core::NodeId::from_raw(e.to.to_raw() + base);
e
})
.collect();
afterautism_adapter::IngestBatch { nodes, edges }
}
fn pick_adapter(path: &std::path::Path) -> Result<Box<dyn Adapter>, String> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
match ext.as_str() {
"csv" | "jsonl" | "ndjson" => Ok(Box::new(RecordsAdapter)),
"md" | "markdown" => Ok(Box::new(MarkdownAdapter)),
other => Err(format!(
"no reference adapter for .{other} (try .csv/.jsonl/.md)"
)),
}
}