use std::path::Path;
use uuid::Uuid;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::project::ProjectLayout;
use crate::store::graph::EndpointRef;
use crate::store::hierarchy::Hierarchy;
use crate::store::Store;
fn parse_uuid(s: &str, what: &str) -> Result<Uuid> {
Uuid::parse_str(s).map_err(|e| Error::Config(format!("invalid {what} uuid `{s}`: {e}")))
}
fn open(project: &Path) -> Result<Store> {
Ok(open_with_cfg(project)?.0)
}
fn open_with_cfg(project: &Path) -> Result<(Store, Config)> {
let layout = ProjectLayout::new(project);
layout.require_initialized()?;
let cfg = Config::load_layered(&layout.config_path())?;
let store = Store::open(layout, &cfg)?;
Ok((store, cfg))
}
pub fn stats(project: &Path) -> Result<()> {
let store = open(project)?;
let s = store.graph_stats()?;
println!("nodes: {}", s.nodes);
println!("edges: {}", s.edges);
if s.by_kind.is_empty() {
println!("(no edges yet — the graph is populated by the SEMNET migrations, P1+)");
} else {
println!("by kind:");
for (kind, n) in &s.by_kind {
println!(" {kind:<16} {n}");
}
}
Ok(())
}
pub fn rebuild(project: &Path) -> Result<()> {
let (store, cfg) = open_with_cfg(project)?;
let r = store.graph_rebuild(&cfg)?;
println!("graph rebuild: cleared {} derivable edge(s), re-derived {}", r.cleared, r.added);
let s = store.graph_stats()?;
println!("graph now holds {} edge(s) across {} node(s)", s.edges, s.nodes);
if !s.by_kind.is_empty() {
for (kind, n) in &s.by_kind {
println!(" {kind:<16} {n}");
}
}
Ok(())
}
pub fn contradicting(project: &Path, node: &str) -> Result<()> {
let store = open(project)?;
let id = parse_uuid(node, "node")?;
let edges = store.contradicting(id)?;
if edges.is_empty() {
println!("no contradictions recorded for {id}");
return Ok(());
}
let here = EndpointRef::Node(id);
for e in &edges {
let (k, r) = e.other_endpoint(&here).as_columns();
let reason = e.reason.as_deref().unwrap_or("");
let sep = if reason.is_empty() { "" } else { " — " };
println!("{} [{}·{}] {k}:{r}{sep}{reason}", e.id, e.kind.as_str(), e.origin.as_str());
}
Ok(())
}
pub fn promote(project: &Path, edge: &str) -> Result<()> {
let store = open(project)?;
let id = parse_uuid(edge, "edge")?;
if store.promote_edge(id)? {
println!("promoted edge {id} (kept across rebuilds)");
} else {
println!("no edge with id {id}");
}
Ok(())
}
pub fn dismiss(project: &Path, edge: &str) -> Result<()> {
let store = open(project)?;
let id = parse_uuid(edge, "edge")?;
store.dismiss_edge(id)?;
println!("dismissed edge {id}");
Ok(())
}
pub fn neighbors(project: &Path, node: &str) -> Result<()> {
let store = open(project)?;
let id = parse_uuid(node, "node")?;
let edges = store.subgraph(id, 1, &[])?;
let h = Hierarchy::load(&store)?;
let label = |ep: &EndpointRef| -> String {
match ep {
EndpointRef::Node(u) => h
.get(*u)
.map(|n| n.title.clone())
.filter(|t| !t.trim().is_empty())
.unwrap_or_else(|| format!("node {}", &u.to_string()[..8])),
EndpointRef::Extern(_) => {
let (k, r) = ep.as_columns();
format!("{k} {r}")
}
}
};
print!("{}", crate::store::graph::render_neighbourhood(id, &edges, label));
Ok(())
}
pub fn loci(project: &Path, node: &str) -> Result<()> {
use crate::store::graph::EdgeKind;
let store = open(project)?;
let id = parse_uuid(node, "node")?;
let edges = store.edges_out(id, &[EdgeKind::CitesLocus])?;
if edges.is_empty() {
println!("{id} cites no primary-source loci");
return Ok(());
}
for e in &edges {
let (_k, r) = e.dst.as_columns();
let key = e.attrs.get("key").and_then(|v| v.as_str()).unwrap_or("");
println!("@{key} {r}");
}
Ok(())
}
pub fn lexical(project: &Path) -> Result<()> {
let (store, cfg) = open_with_cfg(project)?;
let r = store.rebuild_lexical(&cfg)?;
if !r.installed {
let code = crate::ai::prompts::iso_from_long(&cfg.language);
println!("no `{code}` wordnet installed — run `inkhaven wordnet fetch {code}` first");
return Ok(());
}
println!("lexical bridge: cleared {} prior edge(s), imported {}", r.cleared, r.added);
let s = store.graph_stats()?;
for (kind, n) in &s.by_kind {
println!(" {kind:<16} {n}");
}
Ok(())
}
pub fn paths(project: &Path, from: &str, to: &str) -> Result<()> {
use crate::store::graph::EdgeKind;
let store = open(project)?;
let a = parse_uuid(from, "from")?;
let b = parse_uuid(to, "to")?;
match store.paths(a, b, &[EdgeKind::Cites, EdgeKind::LinksTo], 8)? {
Some(path) => {
let hops = path.len().saturating_sub(1);
println!("path found ({hops} hop(s)):");
for id in &path {
println!(" {id}");
}
}
None => println!("no path from {a} to {b} within 8 hops"),
}
Ok(())
}