use std::collections::HashMap;
use std::path::Path;
use miette::{Context, IntoDiagnostic, Result};
use panproto_core::{
gat::{Name, Theory},
inst, lens, protocols,
schema::{Protocol, Schema},
vcs,
};
pub fn load_json<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
let contents = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()))?;
serde_json::from_str(&contents)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse JSON from {}", path.display()))
}
pub fn resolve_protocol(name: &str) -> Result<Protocol> {
match name {
"atproto" => Ok(protocols::atproto::protocol()),
_ => miette::bail!("unknown protocol: {name:?}. Supported: atproto"),
}
}
pub fn build_theory_registry(protocol_name: &str) -> Result<HashMap<String, Theory>> {
let mut registry = HashMap::new();
match protocol_name {
"atproto" => protocols::atproto::register_theories(&mut registry),
_ => miette::bail!(
"unknown protocol for theory registry: {protocol_name:?}. Supported: atproto"
),
}
Ok(registry)
}
pub fn open_repo() -> Result<vcs::Repository> {
let cwd = std::env::current_dir().into_diagnostic()?;
vcs::Repository::open(&cwd)
.into_diagnostic()
.wrap_err("not a panproto repository (or any parent up to mount point)")
}
pub fn parse_defaults(
defaults: &[String],
) -> Result<HashMap<Name, panproto_core::inst::value::Value>> {
let mut map = HashMap::new();
for entry in defaults {
let parts: Vec<&str> = entry.splitn(2, '=').collect();
if parts.len() != 2 {
miette::bail!("invalid default '{entry}': expected 'key=value' format");
}
let key = Name::from(parts[0]);
let value = panproto_core::inst::value::Value::Str(parts[1].to_string());
map.insert(key, value);
}
Ok(map)
}
pub fn infer_root_vertex(schema: &Schema) -> Result<Name> {
let targets: std::collections::HashSet<&Name> = schema.edges.keys().map(|e| &e.tgt).collect();
let root = schema
.vertices
.keys()
.find(|v| !targets.contains(v))
.or_else(|| schema.vertices.keys().next())
.ok_or_else(|| miette::miette!("schema has no vertices"))?;
Ok(root.clone())
}
pub fn auto_lens_result_to_json(result: &lens::AutoLensResult) -> serde_json::Value {
let steps: Vec<serde_json::Value> = result
.chain
.steps
.iter()
.enumerate()
.map(|(i, step)| {
serde_json::json!({
"step": i + 1,
"name": step.name.as_str(),
"lossless": step.is_lossless(),
})
})
.collect();
let coerce_proposals: Vec<serde_json::Value> = result
.coerce_proposals
.iter()
.map(|p| {
serde_json::json!({
"src": p.anchor.src.as_str(),
"tgt": p.anchor.tgt.as_str(),
"witness_name": p.witness_name,
"witness_class": p.witness_class,
"confidence": p.anchor.confidence,
"explanation": p.anchor.explanation,
})
})
.collect();
serde_json::json!({
"alignment_quality": result.alignment_quality,
"steps": steps,
"step_count": result.chain.steps.len(),
"coerce_proposals": coerce_proposals,
})
}
pub fn chain_to_json(chain: &lens::ProtolensChain) -> serde_json::Value {
let steps: Vec<serde_json::Value> = chain
.steps
.iter()
.enumerate()
.map(|(i, step)| {
serde_json::json!({
"step": i + 1,
"name": step.name.as_str(),
"lossless": step.is_lossless(),
})
})
.collect();
serde_json::json!({
"type": "protolens_chain",
"steps": steps,
"step_count": chain.steps.len(),
})
}
pub fn format_timestamp(ts: u64) -> String {
let secs = ts;
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_ymd(days);
format!("{year}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02} UTC")
}
const fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
pub fn parse_range(s: &str) -> Result<(String, String)> {
let parts: Vec<&str> = s.splitn(2, "..").collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
miette::bail!("invalid range '{s}': expected 'old..new' format");
}
Ok((parts[0].to_owned(), parts[1].to_owned()))
}
pub fn load_commit_obj(store: &dyn vcs::Store, id: vcs::ObjectId) -> Result<vcs::CommitObject> {
let obj = store.get(&id).into_diagnostic()?;
match obj {
vcs::Object::Commit(c) => Ok(c),
other => miette::bail!(
"expected commit at {}, found {}",
id.short(),
other.type_name()
),
}
}
pub fn load_schema_from_store(store: &dyn vcs::Store, id: vcs::ObjectId) -> Result<Schema> {
let proto = vcs::tree::project_coproduct_protocol();
vcs::tree::assemble_schema_dyn(store, &id, &proto)
.into_diagnostic()
.wrap_err_with(|| format!("failed to resolve schema at {}", id.short()))
}
pub fn read_json_dir(dir: &Path) -> Result<Vec<std::fs::DirEntry>> {
let mut entries: Vec<_> = std::fs::read_dir(dir)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read directory {}", dir.display()))?
.filter_map(std::result::Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.collect();
entries.sort_by_key(std::fs::DirEntry::file_name);
Ok(entries)
}
pub fn convert_single_file(
path: &Path,
src_schema: &Schema,
tgt_schema: &Schema,
the_lens: &lens::Lens,
direction: &str,
) -> Result<String> {
let data_json: serde_json::Value = load_json(path)?;
let is_forward = direction == "forward";
let forward_schema = if is_forward { src_schema } else { tgt_schema };
let backward_schema = if is_forward { tgt_schema } else { src_schema };
let root_vertex = infer_root_vertex(forward_schema)?;
let instance = inst::parse_json(forward_schema, root_vertex.as_str(), &data_json)
.into_diagnostic()
.wrap_err("failed to parse data as W-type instance")?;
let output_instance = if is_forward {
let (view, _complement) = lens::get(the_lens, &instance)
.into_diagnostic()
.wrap_err("lens get (forward) failed")?;
view
} else {
let complement = lens::Complement::empty();
lens::put(the_lens, &instance, &complement)
.into_diagnostic()
.wrap_err("lens put (backward) failed")?
};
let output = inst::to_json(backward_schema, &output_instance);
serde_json::to_string_pretty(&output)
.into_diagnostic()
.wrap_err("failed to serialize output")
}
pub fn build_schema_model(
schema: &Schema,
name: &str,
theory: &panproto_core::gat::Theory,
) -> panproto_core::gat::Model {
use panproto_core::gat::{GatError, ModelValue};
let mut model = panproto_core::gat::Model::new(name);
for sort in &theory.sorts {
let sort_lower = sort.name.to_lowercase();
let carrier: Vec<ModelValue> = if sort_lower.contains("vertex")
|| sort_lower.contains("node")
|| sort_lower.contains("object")
{
schema
.vertices
.keys()
.map(|k| ModelValue::Str(k.to_string()))
.collect()
} else if sort_lower.contains("edge")
|| sort_lower.contains("arrow")
|| sort_lower.contains("morphism")
{
schema
.edges
.keys()
.map(|e| {
let label = e.name.as_deref().unwrap_or("");
ModelValue::Str(format!("{}→{} {label}", e.src, e.tgt))
})
.collect()
} else {
let config = panproto_core::gat::FreeModelConfig {
max_depth: 2,
max_terms_per_sort: 100,
};
panproto_core::gat::free_model(theory, &config).map_or_else(
|_| Vec::new(),
|result| {
result
.model
.sort_interp
.get(&sort.name.to_string())
.cloned()
.unwrap_or_default()
},
)
};
model.add_sort(sort.name.to_string(), carrier);
}
for op in &theory.ops {
let op_name = op.name.to_string();
let arity = op.arity();
model.add_op(op_name.clone(), move |args: &[ModelValue]| {
if args.len() != arity {
return Err(GatError::ModelError(format!(
"operation '{op_name}' expects {arity} args, got {}",
args.len()
)));
}
let arg_strs: Vec<&str> = args
.iter()
.map(|a| match a {
ModelValue::Str(s) => s.as_str(),
_ => "?",
})
.collect();
Ok(ModelValue::Str(format!(
"{op_name}({})",
arg_strs.join(", ")
)))
});
}
model
}
pub fn print_theory_diff(old_schema: &Schema, new_schema: &Schema) {
type EdgeKey = (String, String, Option<String>);
let old_sorts: std::collections::BTreeSet<&str> =
old_schema.vertices.keys().map(Name::as_str).collect();
let new_sorts: std::collections::BTreeSet<&str> =
new_schema.vertices.keys().map(Name::as_str).collect();
let added_sorts: Vec<&&str> = new_sorts.difference(&old_sorts).collect();
let removed_sorts: Vec<&&str> = old_sorts.difference(&new_sorts).collect();
let edge_key = |e: &panproto_core::schema::Edge| -> EdgeKey {
(
e.src.to_string(),
e.tgt.to_string(),
e.name.as_ref().map(ToString::to_string),
)
};
let old_edges: std::collections::BTreeSet<EdgeKey> =
old_schema.edges.keys().map(edge_key).collect();
let new_edges: std::collections::BTreeSet<EdgeKey> =
new_schema.edges.keys().map(edge_key).collect();
let added_ops: Vec<&EdgeKey> = new_edges.difference(&old_edges).collect();
let removed_ops: Vec<&EdgeKey> = old_edges.difference(&new_edges).collect();
if added_sorts.is_empty()
&& removed_sorts.is_empty()
&& added_ops.is_empty()
&& removed_ops.is_empty()
{
println!("\nTheory diff: no changes.");
return;
}
println!("\nTheory-level diff:");
for s in &added_sorts {
println!(" + sort {s}");
}
for s in &removed_sorts {
println!(" - sort {s}");
}
for (src, tgt, name) in &added_ops {
let label = name.as_deref().unwrap_or("");
println!(" + op {src} -> {tgt} {label}");
}
for (src, tgt, name) in &removed_ops {
let label = name.as_deref().unwrap_or("");
println!(" - op {src} -> {tgt} {label}");
}
}
pub fn print_stored_theory_diff(
store: &dyn panproto_core::vcs::Store,
old_commit: &panproto_core::vcs::CommitObject,
new_commit: &panproto_core::vcs::CommitObject,
old_schema: &Schema,
new_schema: &Schema,
) {
let old_theories = load_theories_from_commit(store, old_commit);
let new_theories = load_theories_from_commit(store, new_commit);
if old_theories.is_empty() && new_theories.is_empty() {
print_theory_diff(old_schema, new_schema);
return;
}
let old_map: std::collections::HashMap<&str, &panproto_core::gat::Theory> =
old_theories.iter().map(|(n, t)| (n.as_str(), t)).collect();
let new_map: std::collections::HashMap<&str, &panproto_core::gat::Theory> =
new_theories.iter().map(|(n, t)| (n.as_str(), t)).collect();
let all_names: std::collections::BTreeSet<&str> =
old_map.keys().chain(new_map.keys()).copied().collect();
let mut has_changes = false;
for name in &all_names {
let lines = diff_theory_pair(old_map.get(name).copied(), new_map.get(name).copied(), name);
if !lines.is_empty() {
if !has_changes {
println!("\nStored theory diff:");
has_changes = true;
}
for line in &lines {
println!("{line}");
}
}
}
if !has_changes {
println!("\nStored theory diff: no changes.");
}
}
fn load_theories_from_commit(
store: &dyn panproto_core::vcs::Store,
commit: &panproto_core::vcs::CommitObject,
) -> Vec<(String, panproto_core::gat::Theory)> {
commit
.theory_ids
.iter()
.filter_map(|(name, id)| {
store.get(id).ok().and_then(|obj| {
if let panproto_core::vcs::Object::Theory(t) = obj {
Some((name.clone(), *t))
} else {
None
}
})
})
.collect()
}
fn diff_theory_pair(
old: Option<&panproto_core::gat::Theory>,
new: Option<&panproto_core::gat::Theory>,
name: &str,
) -> Vec<String> {
let mut lines = Vec::new();
match (old, new) {
(None, Some(t)) => {
lines.push(format!(
" + theory {name} ({} sorts, {} ops)",
t.sorts.len(),
t.ops.len()
));
}
(Some(t), None) => {
lines.push(format!(
" - theory {name} ({} sorts, {} ops)",
t.sorts.len(),
t.ops.len()
));
}
(Some(old_t), Some(new_t)) => {
let old_sorts: std::collections::BTreeSet<&str> =
old_t.sorts.iter().map(|s| s.name.as_ref()).collect();
let new_sorts: std::collections::BTreeSet<&str> =
new_t.sorts.iter().map(|s| s.name.as_ref()).collect();
for s in new_sorts.difference(&old_sorts) {
lines.push(format!(" + sort {s}"));
}
for s in old_sorts.difference(&new_sorts) {
lines.push(format!(" - sort {s}"));
}
let old_ops: std::collections::BTreeSet<&str> =
old_t.ops.iter().map(|o| o.name.as_ref()).collect();
let new_ops: std::collections::BTreeSet<&str> =
new_t.ops.iter().map(|o| o.name.as_ref()).collect();
for o in new_ops.difference(&old_ops) {
lines.push(format!(" + op {o}"));
}
for o in old_ops.difference(&new_ops) {
lines.push(format!(" - op {o}"));
}
if !lines.is_empty() {
lines.insert(0, format!(" theory {name}:"));
}
}
(None, None) => {}
}
lines
}
pub fn print_complement_requirements(
chain: &lens::ProtolensChain,
src_schema: &Schema,
protocol: &Protocol,
) {
let spec = lens::chain_complement_spec(chain, src_schema, protocol);
if !spec.forward_defaults.is_empty() {
println!("Requirements:");
for req in &spec.forward_defaults {
println!(
" + {} ({}, default needed)",
req.element_name, req.element_kind
);
}
}
if !spec.captured_data.is_empty() {
for cap in &spec.captured_data {
println!(" - {} (captured in complement)", cap.element_name);
}
}
}