use crate::*;
pub(crate) fn cmd_parsepolicy(args: &[String]) -> i32 {
let Some(path) = args.first() else {
eprintln!("usage: candor-query parsepolicy <policy-file>");
return 2;
};
let Ok(text) = std::fs::read_to_string(path) else {
eprintln!("candor: cannot read policy {path}");
return 2;
};
let p = candor_classify::policy::parse_policy(&text);
let mut deny: Vec<serde_json::Value> = p
.rules
.iter()
.map(|r| {
serde_json::json!({
"effects": r.effects.iter().copied().collect::<Vec<&str>>(),
"scope": r.scope.as_deref().unwrap_or(""),
})
})
.collect();
let mut allow: Vec<serde_json::Value> = p
.allow_rules
.iter()
.map(|r| {
serde_json::json!({
"effect": r.effect,
"scope": r.scope.as_deref().unwrap_or(""),
"values": r.literals.iter().map(String::as_str).collect::<Vec<&str>>(),
})
})
.collect();
let mut forbid: Vec<serde_json::Value> =
p.layer_rules.iter().map(|r| serde_json::json!({ "from": r.from, "to": r.to })).collect();
deny.sort_by_key(|v| v.to_string());
allow.sort_by_key(|v| v.to_string());
forbid.sort_by_key(|v| v.to_string());
println!("{}", serde_json::json!({ "deny": deny, "allow": allow, "forbid": forbid }));
0
}
pub(crate) fn cmd_whatif(args: &[String]) -> i32 {
if args.len() < 3 {
eprintln!("usage: candor-query whatif <prefix> <fn> <Effect> [policy-file] [0|1]");
return 2;
}
let (prefix, target, effect) = (&args[0], &args[1], &args[2]);
if candor_classify::cap_from_name(effect).is_none() && effect.as_str() != "Unknown" {
eprintln!("candor: unknown effect `{effect}` (expected a candor effect name, e.g. Net/Fs/Db/Exec, or Unknown)");
return 2;
}
let mut policy_path: Option<String> = None;
let mut want_json = false;
for a in &args[3..] {
match a.as_str() {
"0" => want_json = false,
"1" | "--json" => want_json = true,
other => policy_path = Some(other.to_string()),
}
}
if policy_path.is_none() {
policy_path = std::env::var("CANDOR_POLICY").ok();
}
let cg = load_callgraph(prefix);
if cg.is_empty() {
eprintln!("candor: no call-graph sidecar for `{prefix}` — scan the crate first.");
return 2;
}
let mut rev: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (caller, callees) in &cg {
for c in callees {
rev.entry(c.as_str()).or_default().push(caller.as_str());
}
}
let names: BTreeSet<&str> =
cg.keys().map(|s| s.as_str()).chain(cg.values().flatten().map(|s| s.as_str())).collect();
let tier = best_tier(names.iter().copied(), target);
let targets: Vec<&str> = names.iter().copied().filter(|n| q_match(n, target, tier)).collect();
if targets.is_empty() {
eprintln!("candor: no function matching `{target}` in the call graph.");
return 2;
}
let mut affected: BTreeSet<&str> = targets.iter().copied().collect();
let mut stack: Vec<&str> = targets.clone();
while let Some(n) = stack.pop() {
if let Some(cs) = rev.get(n) {
for &c in cs {
if affected.insert(c) {
stack.push(c);
}
}
}
}
let rules = match policy_path.as_deref() {
None => None,
Some(p) => match std::fs::read_to_string(p) {
Ok(t) => Some(candor_classify::policy::parse_policy(&t).rules),
Err(e) => {
eprintln!("candor: policy `{p}` could not be read ({e}) — verdict NOT computed.");
return 2;
}
},
};
let mut violations: Vec<(&str, String)> = Vec::new();
if let Some(rules) = &rules {
for fname in &affected {
for rule in rules {
let denies = if rule.effects.is_empty() {
effect != candor_classify::policy::UNKNOWN
} else {
rule.effects.contains(effect.as_str())
};
let in_scope =
rule.scope.as_deref().is_none_or(|s| candor_classify::policy::scope_matches(fname, s));
if denies && in_scope {
let r = if rule.effects.is_empty() {
format!("pure{}", rule.scope.as_deref().map(|s| format!(" {s}")).unwrap_or_default())
} else {
format!("deny {effect}{}", rule.scope.as_deref().map(|s| format!(" {s}")).unwrap_or_default())
};
violations.push((fname, r));
break;
}
}
}
}
if want_json {
let out = serde_json::json!({
"of": targets,
"effect": effect,
"affected": affected.iter().collect::<Vec<_>>(),
"violations": violations.iter().map(|(f, r)| serde_json::json!({"fn": f, "rule": r})).collect::<Vec<_>>(),
"ok": violations.is_empty(),
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return if violations.is_empty() { 0 } else { 1 };
}
println!("whatif: adding `{effect}` to `{}`", targets.join(", "));
println!(" → propagates to {} function(s) (the blast radius):", affected.len());
for f in &affected {
println!(" {f}");
}
if rules.is_none() {
println!(" (no policy given — pass a policy file or set CANDOR_POLICY for the gate verdict)");
return 0;
}
if violations.is_empty() {
println!(" ✓ within policy — this edit introduces no `deny`/`pure` boundary violation.");
0
} else {
println!(" ⚠ WOULD VIOLATE policy ({}) — run BEFORE the edit:", violations.len());
for (f, r) in &violations {
println!(" [AS-EFF-006] `{f}` (rule: `{r}`)");
}
1
}
}
pub(crate) fn dropped_edges<'a>(
cur: &'a BTreeMap<String, Vec<String>>,
base: &'a BTreeMap<String, Vec<String>>,
) -> BTreeMap<&'a str, Vec<&'a str>> {
let mut dropped: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (caller, base_callees) in base {
let now: BTreeSet<&str> =
cur.get(caller).map(|v| v.iter().map(String::as_str).collect()).unwrap_or_default();
let gone: Vec<&str> = base_callees.iter().map(String::as_str).filter(|c| !now.contains(c)).collect();
if !gone.is_empty() {
dropped.insert(caller.as_str(), gone);
}
}
dropped
}
pub(crate) fn cmd_rewire(args: &[String]) -> i32 {
if args.len() < 2 {
eprintln!("usage: candor-query rewire <cur_prefix> <base_prefix> [0|1]");
return 2;
}
let (cur_pre, base_pre) = (&args[0], &args[1]);
let want_json = args.get(2).map(|s| s == "1").unwrap_or(false);
let cur = load_callgraph(cur_pre);
let base = load_callgraph(base_pre);
if base.is_empty() {
eprintln!("candor: no baseline call graph at `{base_pre}` (need its `.callgraph.json` sidecar).");
return 2;
}
if cur.is_empty() {
eprintln!("candor: no current call graph at `{cur_pre}` (need its `.callgraph.json` sidecar).");
return 2;
}
let dropped = dropped_edges(&cur, &base);
if want_json {
let out = serde_json::json!({
"dropped": dropped.iter().map(|(c, g)| serde_json::json!({"caller": c, "no_longer_calls": g}))
.collect::<Vec<_>>(),
"ok": dropped.is_empty(),
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return if dropped.is_empty() { 0 } else { 1 };
}
if dropped.is_empty() {
println!(" no call edges dropped vs the baseline — nothing de-wired.");
return 0;
}
println!(
" {} function(s) DROPPED a call they made in the baseline — a 'fix' may have disconnected \
functionality (the effect gate can pass while the feature is broken; verify it still works):",
dropped.len()
);
for (caller, gone) in &dropped {
println!(" {caller} ⊘ no longer calls: {}", gone.join(", "));
}
1
}
pub(crate) fn cmd_gate_verdict(args: &[String]) -> i32 {
let (Some(parts), Some(out)) = (args.first(), args.get(1)) else {
eprintln!("usage: candor-query gate-verdict <parts-file> <out-file|->");
return 2;
};
let mut violations: Vec<candor_report::GateViolation> = Vec::new();
match std::fs::read_to_string(parts) {
Ok(text) => {
for line in text.lines().filter(|l| !l.trim().is_empty()) {
match serde_json::from_str(line) {
Ok(v) => violations.push(v),
Err(e) => {
eprintln!("candor-query: corrupt gate record in {parts} ({e}) — no faithful verdict exists");
return 2;
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => {
eprintln!("candor-query: cannot read {parts} ({e})");
return 2;
}
}
let json = match candor_report::gate_verdict_json(&mut violations) {
Ok(j) => j,
Err(e) => {
eprintln!("candor-query: could not serialize the gate verdict ({e})");
return 2;
}
};
if out == "-" {
println!("{json}");
return 0;
}
if let Err(e) = candor_report::write_atomic(Path::new(out), format!("{json}\n").as_bytes()) {
eprintln!("candor-query: could not write the gate verdict to {out} ({e})");
return 2;
}
0
}