use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use candor_report::{report_entries_counted, report_files, ReportEntry, EFFECTS};
use regex::Regex;
use serde::Serialize;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let cmd = args.first().map(String::as_str).unwrap_or("");
let rest = &args[args.len().min(1)..];
let code = match cmd {
"audit" => cmd_audit(rest),
"show" => cmd_show(rest),
"where" => cmd_where(rest),
"callers" => cmd_callers(rest),
"map" => cmd_map(rest),
"diff" => cmd_diff(rest),
"containment" => cmd_containment(rest),
"reachable" => cmd_reachable(rest),
"path" => cmd_path(rest),
"impact" => cmd_impact(rest),
"blindspots" => cmd_blindspots(rest),
"whatif" => cmd_whatif(rest),
"rewire" => cmd_rewire(rest),
"receipt" => cmd_receipt(rest),
"gains" => cmd_gains(rest),
"state" => cmd_state(rest),
"reports" => cmd_reports(rest),
"locate" => cmd_locate(rest),
"engine-version" => cmd_engine_version(rest),
"merge-hook" => cmd_merge_hook(rest),
"parsepolicy" => cmd_parsepolicy(rest),
"--agents" | "agents" => {
println!("<!-- candor-query {} · the agent contract for this installed version -->", env!("CARGO_PKG_VERSION"));
print!("{}", include_str!("../AGENTS.md"));
0
}
other => {
eprintln!(
"candor-query: unknown command '{other}' \
(audit|show|where|callers|map|diff|containment|reachable|path|impact|whatif|rewire|parsepolicy|receipt|gains|state|reports|locate|engine-version|merge-hook|--agents)"
);
2
}
};
std::process::exit(code);
}
fn glob_reports(prefix: &str) -> Vec<PathBuf> {
report_files(prefix).into_iter().map(|r| r.path).collect()
}
fn load_entries(prefix: &str) -> Vec<ReportEntry> {
let mut out = Vec::new();
for path in glob_reports(prefix) {
let Ok(text) = std::fs::read_to_string(&path) else {
eprintln!("candor: report {} could not be read — its functions are OMITTED from this query", path.display());
continue;
};
match report_entries_counted(&text) {
Some((es, dropped)) => {
if dropped > 0 {
eprintln!(
"candor: report {} — {dropped} function entr{} could not be parsed and {} OMITTED from this query (corrupt or mid-write); re-run the scan",
path.display(),
if dropped == 1 { "y" } else { "ies" },
if dropped == 1 { "is" } else { "are" },
);
}
out.extend(es);
}
None => eprintln!(
"candor: report {} failed to parse — its functions are OMITTED from this query (corrupt or mid-write); re-run the scan",
path.display()
),
}
}
out
}
fn load_entries_loud(prefix: &str) -> Result<Vec<ReportEntry>, i32> {
if glob_reports(prefix).is_empty() {
eprintln!("candor: no report files at prefix `{prefix}` — check the path.");
return Err(2);
}
Ok(load_entries(prefix))
}
fn prefix_base(prefix: &str) -> String {
Path::new(prefix).file_name().and_then(|s| s.to_str()).unwrap_or("").to_string()
}
fn glob_encountered(prefix: &str) -> Vec<PathBuf> {
let dir = match Path::new(prefix).parent() {
Some(d) if !d.as_os_str().is_empty() => d.to_path_buf(),
_ => PathBuf::from("."),
};
let base = prefix_base(prefix);
let needle = format!("{base}.encountered-");
let mut out = Vec::new();
if let Ok(rd) = std::fs::read_dir(&dir) {
for ent in rd.flatten() {
if let Some(n) = ent.file_name().to_str()
&& let Some(mid) = n.strip_prefix(&needle).and_then(|m| m.strip_suffix(".json")) {
if !mid.contains('.') {
out.push(ent.path());
}
}
}
}
out
}
fn tally_effects(fns: &[ReportEntry]) -> (BTreeMap<&'static str, usize>, Vec<String>) {
let mut tally: BTreeMap<&'static str, usize> = EFFECTS.iter().map(|e| (*e, 0)).collect();
let mut unresolved: Vec<String> = Vec::new();
for e in fns {
for x in &e.inferred {
if let Some(n) = tally.get_mut(x.as_str()) {
*n += 1;
}
}
if e.unresolved || e.inferred.iter().any(|x| x == "Unknown") {
unresolved.push(e.func.clone());
}
}
(tally, unresolved)
}
fn encountered_set(prefix: &str) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = BTreeSet::new();
for path in glob_encountered(prefix) {
if let Ok(text) = std::fs::read_to_string(&path)
&& let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
seen.extend(arr);
}
}
seen
}
fn gained_effects(cur: &BTreeSet<String>, base: &BTreeSet<String>) -> Vec<String> {
cur.difference(base).cloned().collect()
}
fn cmd_audit(args: &[String]) -> i32 {
let (pre, ver, suspect_path) = match args {
[a, b, c, ..] => (a.as_str(), b.as_str(), c.as_str()),
_ => {
eprintln!("usage: candor-query audit <prefix> <engine_ver> <suspect_file> [--coverage]");
return 2;
}
};
let coverage = args.iter().any(|a| a == "--coverage" || a == "-c");
let base = prefix_base(pre);
let mut fns: Vec<ReportEntry> = Vec::new();
let mut percrate: Vec<(String, usize)> = Vec::new();
let mut analyzed: BTreeSet<String> = BTreeSet::new();
let rfs = report_files(pre);
if rfs.is_empty() {
eprintln!("candor: no report files at prefix `{pre}` — check the path.");
return 2;
}
for rf in rfs {
let label = format!("{}.{}", rf.krate, rf.kind);
analyzed.insert(rf.krate.clone());
let es = match std::fs::read_to_string(&rf.path) {
Ok(t) => match report_entries_counted(&t) {
Some((es, dropped)) => {
if dropped > 0 {
eprintln!("candor: report {} — {dropped} function entr{} could not be parsed and {} OMITTED from this audit (corrupt or mid-write); re-run the scan",
rf.path.display(),
if dropped == 1 { "y" } else { "ies" },
if dropped == 1 { "is" } else { "are" });
}
es
}
None => {
eprintln!("candor: report {} failed to parse — its functions are OMITTED from this audit (corrupt or mid-write); re-run the scan", rf.path.display());
Vec::new()
}
},
Err(_) => {
eprintln!("candor: report {} could not be read — its functions are OMITTED from this audit", rf.path.display());
Vec::new()
}
};
percrate.push((label, es.len()));
fns.extend(es);
}
if fns.is_empty() {
println!("candor: no effectful functions found (everything candor can see is pure).");
return 0;
}
let (tally, unresolved) = tally_effects(&fns);
println!("candor @{ver}");
let pc = percrate.iter().map(|(k, n)| format!("{n} {k}")).collect::<Vec<_>>().join(" · ");
println!("{} effectful functions · {}", fns.len(), pc);
println!();
let mut ranked: Vec<(usize, &str)> = EFFECTS.iter().filter(|e| tally[**e] > 0).map(|e| (tally[*e], *e)).collect();
ranked.sort_by(|a, b| b.cmp(a));
let eff_line = ranked.iter().map(|(n, e)| format!("{n} {e}")).collect::<Vec<_>>().join(" · ");
println!(" effects {eff_line}");
println!();
if !unresolved.is_empty() {
let u: Vec<String> = unresolved.iter().cloned().collect::<BTreeSet<_>>().into_iter().collect();
let extra = if u.len() > 5 { format!(" (+{} more)", u.len() - 5) } else { String::new() };
println!(" ⚠ {} make calls candor can't resolve — their effect set may be incomplete (read these):", u.len());
println!(" {}{extra}", u.iter().take(5).cloned().collect::<Vec<_>>().join(", "));
println!();
}
let (calib_c, calib_p, calib_path) = load_calibrated(pre, &base);
let seen = encountered_set(pre);
let suspect = std::fs::read_to_string(suspect_path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let suspect = suspect.and_then(|s| match Regex::new(&s) {
Ok(re) => Some(re),
Err(e) => {
eprintln!("candor: ignoring unparsable suspect pattern ({e}); coverage-gap check skipped");
None
}
});
let calibrated = |c: &str| -> bool {
calib_c.contains(c)
|| calib_c.iter().any(|k| c == k || c.starts_with(&format!("{k}_")))
|| calib_p.iter().any(|p| c.starts_with(p))
|| calib_path.contains(c) };
let uncovered: Vec<String> =
seen.iter().filter(|c| !calibrated(c) && !analyzed.contains(*c)).cloned().collect();
let suspect_gaps: Vec<String> = match &suspect {
Some(re) => uncovered.iter().filter(|c| re.is_match(c)).cloned().collect(),
None => Vec::new(),
};
if !suspect_gaps.is_empty() {
println!(" ⚠ coverage: {} uncalibrated — effects through them may be under-counted", suspect_gaps.join(", "));
println!();
}
if coverage {
if uncovered.is_empty() {
println!(" coverage: every external crate candor called into is calibrated — no blind spots.");
} else {
println!(" coverage — {} external crate(s) candor has NO effect rules for; calls into them", uncovered.len());
println!(" are assumed PURE, so any I/O they perform is UNDER-REPORTED. Verify the effectful ones:");
for c in &uncovered {
println!(" {c}");
}
}
println!();
} else if !uncovered.is_empty() {
let other = uncovered.len() - suspect_gaps.len();
if other > 0 {
println!(" {other} more external crate(s) have no effect rules (assumed pure) — `cargo candor audit --coverage` to list", );
println!();
}
}
let width = |e: &ReportEntry| e.inferred.iter().filter(|x| *x != "Unknown").count();
let mut top: Vec<&ReportEntry> = fns.iter().filter(|e| width(e) > 0).collect();
top.sort_by(|a, b| width(b).cmp(&width(a)).then_with(|| a.func.cmp(&b.func)));
top.truncate(8);
if !top.is_empty() {
let w = top.iter().map(|e| e.func.chars().count()).max().unwrap_or(0);
println!(" broadest effect surface");
for e in &top {
let effs: Vec<String> = sorted(&e.inferred).into_iter().filter(|x| x != "Unknown").collect();
println!(" {:<w$} {{ {} }}", e.func, effs.join(" "), w = w);
}
println!();
}
println!(" full per-function view: cargo candor audit --all");
if !coverage {
println!(" classifier blind spots: cargo candor audit --coverage");
}
println!(" guard against new effects: cargo candor snapshot .candor/baseline");
0
}
fn load_calibrated(prefix: &str, base: &str) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
let dir = Path::new(prefix).parent().filter(|d| !d.as_os_str().is_empty()).map(|d| d.to_path_buf()).unwrap_or_else(|| PathBuf::from("."));
let path = dir.join(format!("{base}.calibrated.json"));
let Ok(text) = std::fs::read_to_string(&path) else { return Default::default() };
let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else { return Default::default() };
let pick = |key: &str| -> BTreeSet<String> {
v.get(key)
.and_then(|a| a.as_array())
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
.unwrap_or_default()
};
(pick("crates"), pick("prefixes"), pick("path_crates"))
}
#[derive(Serialize)]
struct ShowJson {
#[serde(rename = "fn")]
func: String,
inferred: Vec<String>,
direct: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
hosts: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tables: Vec<String>,
unresolved: bool,
}
fn match_tier(name: &str, q: &str) -> u8 {
if q.is_empty() {
return 0;
}
if name == q {
3
} else if name.ends_with(q)
&& (name[..name.len() - q.len()].ends_with("::") || name[..name.len() - q.len()].ends_with('.'))
{
2
} else if name.contains(q) {
1
} else {
0
}
}
fn name_segments(name: &str) -> Vec<&str> {
if name.contains("::") {
name.split("::").collect()
} else {
name.split('.').collect()
}
}
fn best_tier<'a>(names: impl Iterator<Item = &'a str>, q: &str) -> u8 {
names.map(|n| match_tier(n, q)).max().unwrap_or(0)
}
fn q_match(name: &str, q: &str, tier: u8) -> bool {
tier > 0 && match_tier(name, q) >= tier
}
fn cmd_show(args: &[String]) -> i32 {
let (pre, q, want_json) = match three(args) {
Some(t) => t,
None => {
eprintln!("usage: candor-query show <prefix> <query> <0|1>");
return 2;
}
};
let all = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let tier = best_tier(all.iter().map(|e| e.func.as_str()), q);
let mut fns: Vec<ReportEntry> = all.into_iter().filter(|e| q_match(&e.func, q, tier)).collect();
fns.sort_by(|a, b| a.func.cmp(&b.func));
if want_json {
let out: Vec<ShowJson> = fns
.iter()
.map(|e| ShowJson {
func: e.func.clone(),
inferred: sorted(&e.inferred),
direct: sorted(&e.direct),
fs: e.fs.clone(),
hosts: e.hosts.clone(),
tables: e.tables.clone(),
unresolved: e.unresolved,
})
.collect();
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
if fns.is_empty() {
println!("candor: no effectful function matching `{q}` (pure functions are omitted from the report).");
return 0;
}
let w = fns.iter().map(|e| e.func.chars().count()).max().unwrap_or(0);
let any_fs = fns.iter().any(|e| !e.fs.is_empty());
let any_hosts = fns.iter().any(|e| !e.hosts.is_empty());
for e in &fns {
let direct: BTreeSet<&String> = e.direct.iter().collect();
let parts: Vec<String> = sorted(&e.inferred)
.into_iter()
.map(|x| {
let star = if direct.contains(&x) { "*" } else { "" };
if x == "Fs" && !e.fs.is_empty() {
format!("Fs{star}({})", e.fs.join(","))
} else if x == "Net" && !e.hosts.is_empty() {
format!("Net{star}({})", e.hosts.join(","))
} else if x == "Db" && !e.tables.is_empty() {
format!("Db{star}({})", e.tables.join(","))
} else {
format!("{x}{star}")
}
})
.collect();
let unk = if e.unresolved { " ⚠ unresolved (set may be incomplete)" } else { "" };
println!(" {:<w$} {{ {} }}{}", e.func, parts.join(" "), unk, w = w);
}
let fs_note = if any_fs { "; Fs(read/write) = the filesystem access seen" } else { "" };
let host_note = if any_hosts { "; Net(host) = a literal endpoint seen (runtime addresses aren't shown)" } else { "" };
let any_tables = fns.iter().any(|e| !e.tables.is_empty());
let table_note = if any_tables { "; Db(table) = a literal table seen (dynamic SQL isn't shown)" } else { "" };
println!(" (* = performed in the function's own body; unmarked = via a callee{fs_note}{host_note}{table_note})");
0
}
#[derive(Serialize)]
struct WhereJson {
effect: String,
directly: Vec<String>,
inherited: Vec<String>,
}
fn cmd_where(args: &[String]) -> i32 {
let (pre, eff, want_json) = match three(args) {
Some(t) => t,
None => {
eprintln!("usage: candor-query where <prefix> <Effect> <0|1>");
return 2;
}
};
let all = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let mut direct: Vec<String> =
all.iter().filter(|e| e.direct.iter().any(|x| x == eff)).map(|e| e.func.clone()).collect();
let mut inherit: Vec<String> = all
.iter()
.filter(|e| e.inferred.iter().any(|x| x == eff) && !e.direct.iter().any(|x| x == eff))
.map(|e| e.func.clone())
.collect();
direct.sort();
inherit.sort();
if want_json {
let out = WhereJson { effect: eff.to_string(), directly: direct, inherited: inherit };
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
if direct.is_empty() && inherit.is_empty() {
println!("candor: no function performs {eff} in the report.");
return 0;
}
println!("{} function(s) perform {eff}:", direct.len() + inherit.len());
if !direct.is_empty() {
println!(" directly ({}):", direct.len());
for fn_ in &direct {
println!(" {fn_}");
}
}
if !inherit.is_empty() {
println!(" inherit it via a callee ({}):", inherit.len());
for fn_ in &inherit {
println!(" {fn_}");
}
}
0
}
fn cmd_callers(args: &[String]) -> i32 {
let include_unknown = args.iter().any(|a| a == "--include-unknown");
let pos: Vec<String> = args.iter().filter(|a| *a != "--include-unknown").cloned().collect();
let (pre, q, want_json) = match three(&pos) {
Some(t) => t,
None => {
eprintln!("usage: candor-query callers <prefix> <query> <0|1> [--include-unknown]");
return 2;
}
};
let mut cg = load_callgraph(pre);
if cg.is_empty() {
cg = match load_entries_loud(pre) {
Ok(v) => v.into_iter().map(|e| (e.func, e.calls)).collect(),
Err(c) => return c,
};
}
if include_unknown {
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
callers_via_callgraph_frontier(&cg, &entries, &load_hierarchy(pre), q, want_json)
} else {
callers_via_callgraph(&cg, q, want_json)
}
}
fn simple_method(f: &str) -> &str {
f.rfind('.').map(|i| &f[i + 1..]).unwrap_or(f)
}
fn declaring_type(f: &str) -> &str {
f.rfind('.').map(|i| &f[..i]).unwrap_or(f)
}
fn is_subtype_of(ty: &str, owner: &str, hier: &BTreeMap<String, Vec<String>>) -> bool {
if ty == owner {
return true;
}
let mut seen: HashSet<&str> = HashSet::new();
let mut stack: Vec<&str> = vec![ty];
while let Some(t) = stack.pop() {
if let Some(sups) = hier.get(t) {
for s in sups {
if s == owner {
return true;
}
if seen.insert(s.as_str()) {
stack.push(s.as_str());
}
}
}
}
false
}
fn load_hierarchy(prefix: &str) -> BTreeMap<String, Vec<String>> {
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
let p = Path::new(prefix);
let dir = p
.parent()
.filter(|d| !d.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let Some(base) = p.file_name().and_then(|s| s.to_str()) else {
return out;
};
let pfx = format!("{base}.");
let Ok(rd) = std::fs::read_dir(&dir) else {
return out;
};
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(&pfx) || !name.ends_with(".hierarchy.json") {
continue;
}
if let Ok(text) = std::fs::read_to_string(ent.path())
&& let Ok(map) = serde_json::from_str::<BTreeMap<String, Vec<String>>>(&text)
{
for (k, v) in map {
out.entry(k).or_default().extend(v);
}
}
}
out
}
fn callers_via_callgraph_frontier(
cg: &BTreeMap<String, Vec<String>>,
entries: &[ReportEntry],
hier: &BTreeMap<String, Vec<String>>,
q: &str,
want_json: bool,
) -> i32 {
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(), q);
let targets: Vec<String> = names.iter().copied().filter(|n| q_match(n, q, tier)).map(String::from).collect();
if targets.is_empty() {
if want_json {
println!("{{}}");
} else {
println!("candor: no function matching `{q}` found in the call graph.");
}
return 0;
}
let direct: BTreeSet<String> =
targets.iter().flat_map(|t| rev.get(t.as_str()).into_iter().flatten().map(|s| s.to_string())).collect();
let mut all: BTreeSet<String> = BTreeSet::new();
let mut stack: Vec<String> = targets.clone();
while let Some(n) = stack.pop() {
if let Some(cs) = rev.get(n.as_str()) {
for &c in cs {
if all.insert(c.to_string()) {
stack.push(c.to_string());
}
}
}
}
let mut confirmed: BTreeSet<&str> = BTreeSet::new();
for t in &targets {
confirmed.insert(t.as_str());
}
for a in &all {
confirmed.insert(a.as_str());
}
let mut by_method: HashMap<&str, Vec<&str>> = HashMap::new();
for r in &confirmed {
by_method.entry(simple_method(r)).or_default().push(declaring_type(r));
}
let has_hier = !hier.is_empty();
let mut possible: Vec<(String, String)> = Vec::new();
for e in entries {
if confirmed.contains(e.func.as_str()) {
continue;
}
let mut hits: BTreeSet<&str> = BTreeSet::new();
for w in &e.unknown_why {
if let Some(key) = w.strip_prefix("dispatch:") {
let m = simple_method(key);
let owner = declaring_type(key);
if let Some(types) = by_method.get(m)
&& (!has_hier || types.iter().any(|t| is_subtype_of(t, owner, hier)))
{
hits.insert(m);
}
}
}
if !hits.is_empty() {
possible.push((e.func.clone(), hits.iter().copied().collect::<Vec<_>>().join(",")));
}
}
possible.sort();
if want_json {
let pv: Vec<_> =
possible.iter().map(|(f, v)| serde_json::json!({"fn": f, "viaDispatchOn": v})).collect();
let out = serde_json::json!({
"of": targets,
"direct": direct.iter().collect::<Vec<_>>(),
"transitive": all.iter().collect::<Vec<_>>(),
"possibleViaUnknownDispatch": pv,
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
let tgt = targets.join(", ");
if !all.is_empty() {
println!(" `{tgt}` is reached by {} function(s) (the blast radius if it gained an effect):", all.len());
for c in &all {
let mark = if direct.contains(c) { " (direct)" } else { "" };
println!(" {c}{mark}");
}
}
if !possible.is_empty() {
println!(" + {} function(s) MAY also reach `{tgt}` via an unresolved broad dispatch candor declined to resolve (cannot confirm):", possible.len());
for (f, v) in &possible {
println!(" {f} (via dispatch on {v})");
}
}
if all.is_empty() && possible.is_empty() {
println!(" `{tgt}` has no callers (nothing in this crate calls it).");
}
0
}
fn callers_via_callgraph(cg: &BTreeMap<String, Vec<String>>, q: &str, want_json: bool) -> i32 {
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(), q);
let targets: Vec<&str> = names.iter().copied().filter(|n| q_match(n, q, tier)).collect();
if targets.is_empty() {
if want_json {
println!("{{}}");
} else {
println!("candor: no function matching `{q}` found in the call graph.");
}
return 0;
}
let direct: BTreeSet<&str> = targets.iter().flat_map(|t| rev.get(t).into_iter().flatten().copied()).collect();
let mut all: BTreeSet<&str> = BTreeSet::new();
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 all.insert(c) {
stack.push(c);
}
}
}
}
if want_json {
let out = serde_json::json!({
"of": targets,
"direct": direct.iter().collect::<Vec<_>>(),
"transitive": all.iter().collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
let tgt = targets.join(", ");
if all.is_empty() {
println!(" `{tgt}` has no callers (nothing in this crate calls it).");
return 0;
}
println!(
" `{tgt}` is reached by {} function(s) (the blast radius if it gained an effect):",
all.len()
);
for c in &all {
let mark = if direct.contains(c) { " (direct)" } else { "" };
println!(" {c}{mark}");
}
0
}
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
}
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 = rule.effects.is_empty() || 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
}
}
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
}
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
}
fn load_callgraph(prefix: &str) -> BTreeMap<String, Vec<String>> {
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
let p = Path::new(prefix);
let dir = p.parent().filter(|d| !d.as_os_str().is_empty()).map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
let Some(base) = p.file_name().and_then(|s| s.to_str()) else { return out };
let pfx = format!("{base}.");
let Ok(rd) = std::fs::read_dir(&dir) else { return out };
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(&pfx) || !name.ends_with(".callgraph.json") {
continue;
}
let path = ent.path();
let Ok(text) = std::fs::read_to_string(&path) else {
eprintln!("candor: callgraph {} could not be read — its edges are OMITTED, so blast-radius queries (whatif/rewire/callers) UNDER-report", path.display());
continue;
};
match serde_json::from_str::<BTreeMap<String, Vec<String>>>(&text) {
Ok(map) => {
for (k, v) in map {
out.entry(k).or_default().extend(v);
}
}
Err(_) => eprintln!("candor: callgraph {} failed to parse — its edges are OMITTED, so blast-radius queries (whatif/rewire/callers) UNDER-report (corrupt or mid-write); re-run the scan", path.display()),
}
}
out
}
#[derive(Serialize)]
struct MapJson {
effects: Vec<String>,
functions: usize,
}
fn cmd_map(args: &[String]) -> i32 {
let (pre, want_json) = match two(args) {
Some(t) => t,
None => {
eprintln!("usage: candor-query map <prefix> <0|1>");
return 2;
}
};
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let mut mods: BTreeMap<String, (BTreeSet<String>, usize)> = BTreeMap::new();
for e in entries {
let stripped = e.func.strip_prefix('<').unwrap_or(&e.func);
let end = if stripped.contains("::") {
[stripped.find(" as "), stripped.find('>'), stripped.find("::")]
.into_iter()
.flatten()
.min()
.unwrap_or(stripped.len())
} else {
stripped.rfind('.').unwrap_or(stripped.len())
};
let m = if end == stripped.len() {
"(root)".to_string()
} else {
match stripped[..end].trim() {
"" => "(root)".to_string(),
s => s.to_string(),
}
};
let v = mods.entry(m).or_default();
v.0.extend(e.inferred.iter().filter(|x| *x != "Unknown").cloned());
v.1 += 1;
}
if want_json {
let out: BTreeMap<String, MapJson> = mods
.iter()
.map(|(m, (eff, n))| (m.clone(), MapJson { effects: eff.iter().cloned().collect(), functions: *n }))
.collect();
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
if mods.is_empty() {
println!("candor: no effectful functions in the report.");
return 0;
}
let total: usize = mods.values().map(|(_, n)| *n).sum();
println!("candor map — {total} effectful functions across {} module(s)", mods.len());
println!();
let w = mods.keys().map(|m| m.chars().count()).max().unwrap_or(0);
let mut order: Vec<&String> = mods.keys().collect();
order.sort_by(|a, b| {
let (na, nb) = (mods[*a].1, mods[*b].1);
nb.cmp(&na).then_with(|| a.cmp(b))
});
for m in order {
let (eff, n) = &mods[m];
let effs: Vec<String> = eff.iter().cloned().collect();
let s = if *n != 1 { "s" } else { "" };
println!(" {:<w$} {{ {} }} ({} fn{})", m, effs.join(" "), n, s, w = w);
}
0
}
#[derive(Default, Clone)]
struct FnInfo {
inferred: BTreeSet<String>,
direct: BTreeSet<String>,
calls: BTreeSet<String>,
}
fn load_fninfo(prefix: &str) -> BTreeMap<String, FnInfo> {
let mut out: BTreeMap<String, FnInfo> = BTreeMap::new();
for e in load_entries(prefix) {
let info = out.entry(e.func.clone()).or_default();
info.inferred.extend(e.inferred);
info.direct.extend(e.direct);
info.calls.extend(e.calls);
}
out
}
#[derive(Serialize, Clone)]
struct Change {
#[serde(rename = "fn")]
func: String,
gained: Vec<String>,
introduced: Vec<String>,
inherited: Vec<String>,
lost: Vec<String>,
status: String,
}
#[derive(Serialize)]
struct DiffJson<'a> {
baseline_version: &'a str,
engine_version: &'a str,
changes: Vec<Change>,
}
fn cmd_diff(args: &[String]) -> i32 {
if args.len() < 5 {
eprintln!("usage: candor-query diff <cur_prefix> <base_prefix> <0|1> <baseline_ver> <engine_ver>");
return 2;
}
let (cur_pre, base_pre, want_json, bver, ever) =
(&args[0], &args[1], args[2] == "1", args[3].as_str(), args[4].as_str());
for (which, pre) in [("current", cur_pre), ("baseline", base_pre)] {
if glob_reports(pre).is_empty() {
eprintln!("candor: no report files at {which} prefix `{pre}` — check the path.");
return 2;
}
}
let cur = load_fninfo(cur_pre);
let base = load_fninfo(base_pre);
let empty = BTreeSet::new();
let mut changes: Vec<Change> = Vec::new();
let keys: BTreeSet<&String> = cur.keys().chain(base.keys()).collect();
for fn_ in keys {
let ci = cur.get(fn_).map(|v| &v.inferred).unwrap_or(&empty);
let bi = base.get(fn_).map(|v| &v.inferred).unwrap_or(&empty);
if !cur.contains_key(fn_) {
if !bi.is_empty() {
changes.push(Change {
func: fn_.clone(),
gained: vec![],
introduced: vec![],
inherited: vec![],
lost: bi.iter().cloned().collect(),
status: "removed".into(),
});
}
continue;
}
let gained: Vec<String> = gained_effects(ci, bi);
let lost: Vec<String> =
if base.contains_key(fn_) { bi.difference(ci).cloned().collect() } else { vec![] };
if gained.is_empty() && lost.is_empty() {
continue;
}
let cd = &cur[fn_].direct;
let introduced: Vec<String> = gained.iter().filter(|e| cd.contains(*e)).cloned().collect();
let inherited: Vec<String> = gained.iter().filter(|e| !cd.contains(*e)).cloned().collect();
changes.push(Change {
func: fn_.clone(),
gained,
introduced,
inherited,
lost,
status: if base.contains_key(fn_) { "changed".into() } else { "new".into() },
});
}
if want_json {
changes.sort_by(|a, b| a.func.cmp(&b.func));
let out = DiffJson { baseline_version: bver, engine_version: ever, changes };
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
if changes.is_empty() {
println!("candor: no effect changes vs {base_pre} (@{}).", q_or(bver));
return 0;
}
println!("candor diff — current (@{ever}) vs {base_pre} (@{})", q_or(bver));
if !bver.is_empty() && !ever.is_empty() && bver != ever {
println!(" ⚠ baseline @{bver} ≠ engine @{ever} — some changes may be the engine reclassifying, not your code.");
}
println!();
let calls_of: BTreeMap<&String, &BTreeSet<String>> =
cur.iter().map(|(fn_, v)| (fn_, &v.calls)).collect();
let sources: BTreeSet<String> =
changes.iter().filter(|c| !c.introduced.is_empty()).map(|c| c.func.clone()).collect();
let all_effects: BTreeSet<String> = changes.iter().flat_map(|c| c.gained.iter().cloned()).collect();
let mut top_level: BTreeSet<String> = BTreeSet::new();
let mut endpoints_of: BTreeMap<String, Vec<String>> = BTreeMap::new();
for e in &all_effects {
let gainers: BTreeSet<String> =
changes.iter().filter(|c| c.gained.contains(e)).map(|c| c.func.clone()).collect();
let mut called: BTreeSet<String> = BTreeSet::new();
for g in &gainers {
if let Some(cs) = calls_of.get(g) {
called.extend(cs.intersection(&gainers).cloned());
}
}
let tl: BTreeSet<String> = gainers.difference(&called).cloned().collect();
top_level.extend(tl.iter().cloned());
endpoints_of.insert(e.clone(), tl.difference(&sources).cloned().collect());
}
let consequential: BTreeSet<String> = sources.union(&top_level).cloned().collect();
changes.sort_by(|a, b| rank(a, &top_level).cmp(&rank(b, &top_level)).then_with(|| a.func.cmp(&b.func)));
let mut shown: BTreeSet<String> = BTreeSet::new();
for c in &changes {
if !consequential.contains(&c.func) && c.lost.is_empty() {
continue; }
shown.insert(c.func.clone());
let mut parts: Vec<String> = Vec::new();
parts.extend(c.introduced.iter().map(|e| format!("+{e}*")));
parts.extend(c.inherited.iter().map(|e| format!("+{e}")));
parts.extend(c.lost.iter().map(|e| format!("-{e}")));
let mark = if !c.gained.is_empty() { "+" } else { "-" };
let mut tags: Vec<&str> = Vec::new();
if c.status == "new" {
tags.push("new fn");
}
if c.status == "removed" {
tags.push("removed fn");
}
if top_level.contains(&c.func) && !sources.contains(&c.func) {
tags.push("top-level");
}
if c.gained.iter().any(|e| e == "Unknown") {
tags.push("⚠ now unresolvable");
}
let tag = if tags.is_empty() { String::new() } else { format!(" ({})", tags.join(", ")) };
println!(" {mark} {}{tag} {{ {} }}", c.func, parts.join(" "));
}
let mut hidden: Vec<String> =
changes.iter().filter(|c| !shown.contains(&c.func)).map(|c| c.func.clone()).collect();
hidden.sort();
if !hidden.is_empty() {
let head: Vec<String> = hidden.iter().take(4).cloned().collect();
let names = if hidden.len() > 4 {
format!("{}, +{} more", head.join(", "), hidden.len() - 4)
} else {
head.join(", ")
};
println!(" … {} intermediate caller(s) also inherit it: {names}", hidden.len());
}
println!();
println!(" * = introduced here; (top-level) = where the effect surfaces (an entry point / public API).");
for e in &all_effects {
let mut srcs: Vec<String> =
changes.iter().filter(|c| c.introduced.contains(e)).map(|c| c.func.clone()).collect();
srcs.sort();
let gainers: BTreeSet<String> =
changes.iter().filter(|c| c.gained.contains(e)).map(|c| c.func.clone()).collect();
let eps = endpoints_of.get(e).cloned().unwrap_or_default();
if !srcs.is_empty() {
let reach = if !eps.is_empty() { format!(" → reaches {}", eps.join(", ")) } else { String::new() };
let extra = gainers.len() as i64 - srcs.len() as i64 - eps.len() as i64;
let tail = if extra > 0 {
format!(" (+{extra} intermediate)")
} else if eps.is_empty() {
" (stays local)".to_string()
} else {
String::new()
};
println!(" {e}: introduced in {}{reach}{tail}", srcs.join(", "));
} else if !gainers.is_empty() {
let names = if !eps.is_empty() { eps.join(", ") } else { gainers.iter().cloned().collect::<Vec<_>>().join(", ") };
println!(" {e}: reaches {names} (source outside this crate/baseline)");
}
}
if changes.iter().any(|c| c.gained.iter().any(|e| e == "Unknown")) {
println!(" ⚠ a new Unknown means candor can no longer prove that function's effect set is complete — review it.");
}
0
}
fn rank(c: &Change, top_level: &BTreeSet<String>) -> u8 {
if !c.introduced.is_empty() {
0
} else if top_level.contains(&c.func) {
1
} else if !c.gained.is_empty() {
2
} else {
3
}
}
const CONTAINED: &[&str] = &["Db", "Net", "Exec", "Fs", "Ipc"];
const AMBIENT: &[&str] = &["Log", "Clock", "Rand", "Env"];
fn norm_name(name: &str) -> &str {
match name.strip_prefix('<') {
Some(rest) => rest.split(" as ").next().unwrap_or(rest),
None => name,
}
}
fn common_prefix_len(names: &[&String]) -> usize {
let mut prefix: Option<Vec<&str>> = None;
for n in names {
let segs: Vec<&str> = name_segments(norm_name(n)); match &mut prefix {
None => prefix = Some(segs),
Some(p) => {
let mut i = 0;
while i < p.len() && i < segs.len() && p[i] == segs[i] {
i += 1;
}
p.truncate(i);
}
}
}
prefix.map(|p| p.len()).unwrap_or(0)
}
fn layer_of(name: &str, prefix_len: usize) -> String {
let segs: Vec<&str> = name_segments(norm_name(name));
if prefix_len + 1 < segs.len() {
segs[prefix_len].to_string()
} else {
"(root)".to_string()
}
}
fn cmd_blindspots(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let Some(pre) = pos.first() else {
eprintln!("usage: candor-query blindspots <prefix> [--json]");
return 2;
};
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let mut rev: HashMap<&str, Vec<&str>> = HashMap::new();
for e in &entries {
for c in &e.calls {
rev.entry(c.as_str()).or_default().push(e.func.as_str());
}
}
let total_unknown = entries.iter().filter(|e| e.inferred.iter().any(|x| x == "Unknown")).count();
#[derive(Serialize)]
struct Source {
#[serde(rename = "fn")]
func: String,
why: Vec<String>,
reaches: usize,
affected: Vec<String>,
}
let mut sources: Vec<Source> = Vec::new();
for e in &entries {
if e.unknown_why.is_empty() {
continue; }
let mut seen: HashSet<&str> = HashSet::new();
let mut q: VecDeque<&str> = VecDeque::new();
q.push_back(e.func.as_str());
seen.insert(e.func.as_str());
while let Some(cur) = q.pop_front() {
if let Some(callers) = rev.get(cur) {
for &caller in callers {
if seen.insert(caller) {
q.push_back(caller);
}
}
}
}
let mut affected: Vec<String> =
seen.iter().copied().filter(|n| *n != e.func.as_str()).map(String::from).collect();
affected.sort_unstable();
sources.push(Source { func: e.func.clone(), why: e.unknown_why.clone(), reaches: affected.len(), affected });
}
sources.sort_by(|a, b| b.reaches.cmp(&a.reaches).then_with(|| a.func.cmp(&b.func)));
if want_json {
#[derive(Serialize)]
struct Out {
sources: Vec<Source>,
#[serde(rename = "totalUnknown")]
total_unknown: usize,
}
println!("{}", serde_json::to_string(&Out { sources, total_unknown }).unwrap());
return 0;
}
if sources.is_empty() {
println!(" no Unknown sources — every call resolved (or no Unknown in this report).");
return 0;
}
println!(
" {} Unknown source(s) explaining {} Unknown function(s) — the blind spots to declare, resolve, or accept:",
sources.len(), total_unknown
);
for s in &sources {
println!(" {:<52} reaches {:>4} {:?}", s.func, s.reaches, s.why);
}
0
}
fn cmd_impact(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let (Some(pre), Some(fn_arg)) = (pos.first(), pos.get(1)) else {
eprintln!("usage: candor-query impact <prefix> <fn-substring> [--json]");
return 2;
};
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let by_name: HashMap<&str, &ReportEntry> =
entries.iter().map(|e| (e.func.as_str(), e)).collect();
let target = entries
.iter()
.find(|e| &e.func == *fn_arg)
.or_else(|| entries.iter().find(|e| e.func.contains(fn_arg.as_str())));
let Some(target) = target else {
eprintln!("candor-query impact: no function matching '{fn_arg}'");
return 2;
};
let mut rev: HashMap<&str, Vec<&str>> = HashMap::new();
for e in &entries {
for c in &e.calls {
rev.entry(c.as_str()).or_default().push(e.func.as_str());
}
}
let mut seen: HashSet<&str> = HashSet::new();
let mut q: VecDeque<&str> = VecDeque::new();
q.push_back(target.func.as_str());
seen.insert(target.func.as_str());
while let Some(cur) = q.pop_front() {
if let Some(callers) = rev.get(cur) {
for &caller in callers {
if seen.insert(caller) {
q.push_back(caller);
}
}
}
}
let mut affected_names: Vec<&str> =
seen.iter().copied().filter(|n| *n != target.func.as_str()).collect();
affected_names.sort_unstable();
let mut roots: Vec<&ReportEntry> = Vec::new();
if target.entry_point {
roots.push(target);
}
let mut downstream: Vec<&ReportEntry> = seen
.iter()
.filter(|n| **n != target.func.as_str())
.filter_map(|n| by_name.get(n).copied())
.filter(|e| e.entry_point)
.collect();
downstream.sort_by(|a, b| a.func.cmp(&b.func));
roots.extend(downstream);
if want_json {
let eps: Vec<_> = roots
.iter()
.map(|r| serde_json::json!({ "fn": r.func, "inferred": r.inferred }))
.collect();
let out = serde_json::json!({
"fn": target.func,
"affectedCount": affected_names.len(),
"affected": affected_names,
"entryPoints": eps
});
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
println!("candor impact — what changing `{}` affects:\n", target.func);
println!(
" {} effectful function{} transitively call it.",
affected_names.len(),
if affected_names.len() == 1 { "" } else { "s" }
);
if roots.is_empty() {
println!(
" No entry point reaches it — not on a runtime path (dead, or a library fn called only externally)."
);
return 0;
}
println!(
" {} entry point{} downstream (a change here surfaces at runtime via):",
roots.len(),
if roots.len() == 1 { "" } else { "s" }
);
for r in &roots {
println!(" {} {{ {} }}", r.func, r.inferred.join(", "));
}
0
}
fn cmd_path(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let (Some(pre), Some(fn_arg), Some(effect)) = (pos.first(), pos.get(1), pos.get(2)) else {
eprintln!("usage: candor-query path <prefix> <fn-substring> <Effect> [--json]");
return 2;
};
let effect = effect.as_str();
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let by_name: HashMap<&str, &ReportEntry> =
entries.iter().map(|e| (e.func.as_str(), e)).collect();
let start = entries
.iter()
.find(|e| &e.func == *fn_arg)
.or_else(|| entries.iter().find(|e| e.func.contains(fn_arg.as_str())));
let Some(start) = start else {
eprintln!("candor-query path: no function matching '{fn_arg}'");
return 2;
};
if !start.inferred.iter().any(|e| e == effect) {
if want_json {
let out = serde_json::json!({ "fn": start.func, "effect": effect, "path": [] });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
} else {
println!("{} does not perform {effect} (inferred: {:?})", start.func, start.inferred);
}
return 0;
}
let mut prev: HashMap<&str, Option<&str>> = HashMap::new();
let mut q: VecDeque<&str> = VecDeque::new();
q.push_back(start.func.as_str());
prev.insert(start.func.as_str(), None);
let mut source: Option<&str> = None;
while let Some(cur) = q.pop_front() {
let Some(f) = by_name.get(cur) else { continue };
if f.direct.iter().any(|e| e == effect) {
source = Some(cur);
break;
}
for c in &f.calls {
if let Some(cf) = by_name.get(c.as_str())
&& cf.inferred.iter().any(|e| e == effect) && !prev.contains_key(c.as_str()) {
prev.insert(c.as_str(), Some(cur));
q.push_back(c.as_str());
}
}
}
let Some(source) = source else {
if want_json {
let out = serde_json::json!({ "fn": start.func, "effect": effect, "path": [] });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
} else {
println!(
"{} performs {effect} but its source is not a local function \
(cross-crate, or via Unknown) — not statically traceable.",
start.func
);
}
return 0;
};
let mut chain: Vec<&str> = Vec::new();
let mut n = Some(source);
while let Some(name) = n {
chain.push(name);
n = *prev.get(name).unwrap();
}
chain.reverse();
if want_json {
let steps: Vec<_> = chain
.iter()
.enumerate()
.map(|(i, name)| {
let loc = by_name.get(name).map(|e| e.loc.clone()).unwrap_or_default();
serde_json::json!({ "fn": name, "loc": loc, "source": i == chain.len() - 1 })
})
.collect();
let out = serde_json::json!({ "fn": start.func, "effect": effect, "path": steps });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
println!("candor path — how `{}` comes to perform {effect}:\n", start.func);
for (i, name) in chain.iter().enumerate() {
let indent = " ".repeat(i + 1);
let arrow = if i == 0 { "" } else { "→ " };
let tag = if i == chain.len() - 1 {
let loc = by_name.get(name).map(|e| e.loc.as_str()).unwrap_or("");
if loc.is_empty() {
format!(" [{effect} source]")
} else {
format!(" [{effect} source @ {loc}]")
}
} else {
String::new()
};
println!("{indent}{arrow}{name}{tag}");
}
0
}
fn cmd_reachable(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let Some(pre) = pos.first() else {
eprintln!("usage: candor-query reachable <prefix> [--json]");
return 2;
};
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let roots: Vec<&ReportEntry> = entries.iter().filter(|e| e.entry_point).collect();
let mut by_eff: BTreeMap<String, Vec<String>> = BTreeMap::new();
for e in &roots {
for eff in &e.inferred {
by_eff.entry(eff.clone()).or_default().push(e.func.clone());
}
}
if want_json {
let effects: serde_json::Map<String, serde_json::Value> = by_eff
.iter()
.map(|(eff, who)| {
(eff.clone(), serde_json::json!({ "count": who.len(), "via": who }))
})
.collect();
let out = serde_json::json!({ "entryPoints": roots.len(), "effects": effects });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
println!(
"candor reachable — effects the program performs at runtime (union over {} entry point{})",
roots.len(),
if roots.len() == 1 { "" } else { "s" }
);
if roots.is_empty() {
println!(" (no entry points in this report — nothing is marked runtime-invoked)");
return 0;
}
let order: Vec<&str> = CONTAINED
.iter()
.chain(AMBIENT.iter())
.copied()
.chain(["Clipboard", "Unknown"])
.collect();
let mut seen: Vec<&String> = by_eff.keys().collect();
seen.sort_by_key(|e| order.iter().position(|o| *o == e.as_str()).unwrap_or(order.len()));
for eff in seen {
let who = &by_eff[eff];
let examples =
who.iter().take(3).map(|s| reachable_leaf(s)).collect::<Vec<_>>().join(", ");
let more = if who.len() > 3 { ", …" } else { "" };
let tag = if eff == "Unknown" { " ← visibility caveat, not a performed effect" } else { "" };
println!(" {eff:<10} {:>3} ({examples}{more}){tag}", who.len());
}
let pure = roots.iter().filter(|e| e.inferred.is_empty()).count();
let n = roots.len();
println!("\n {n} entry point{}; {pure} perform no effect (pure roots).", if n == 1 { "" } else { "s" });
0
}
fn reachable_leaf(fname: &str) -> String {
let mut segs: Vec<&str> = fname.rsplitn(3, "::").take(2).collect();
segs.reverse();
segs.join("::")
}
fn cmd_containment(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let Some(cur_pre) = pos.first() else {
eprintln!("usage: candor-query containment <prefix> [baseline_prefix] [--json]");
return 2;
};
if glob_reports(cur_pre).is_empty() {
eprintln!("candor: no report files at prefix `{cur_pre}` — check the path.");
return 2;
}
let cur = load_fninfo(cur_pre);
let names: Vec<&String> = cur.keys().collect();
let pl = common_prefix_len(&names);
let mut by_eff: BTreeMap<&'static str, BTreeMap<String, usize>> = BTreeMap::new();
let known: Vec<&'static str> = CONTAINED.iter().chain(AMBIENT.iter()).copied().collect();
for (fname, info) in &cur {
let layer = layer_of(fname, pl);
for eff in &info.direct {
if let Some(k) = known.iter().find(|e| **e == eff.as_str()) {
*by_eff.entry(*k).or_default().entry(layer.clone()).or_default() += 1;
}
}
}
if let Some(base_pre) = pos.get(1) {
let base = load_fninfo(base_pre);
let bnames: Vec<&String> = base.keys().collect();
let bpl = common_prefix_len(&bnames);
let mut base_layers: BTreeMap<&str, BTreeSet<String>> = BTreeMap::new();
for (fname, info) in &base {
let layer = layer_of(fname, bpl);
for eff in &info.direct {
if let Some(k) = CONTAINED.iter().find(|e| **e == eff.as_str()) {
base_layers.entry(*k).or_default().insert(layer.clone());
}
}
}
let mut leaks: Vec<String> = Vec::new();
let mut cleanups: Vec<String> = Vec::new();
for eff in CONTAINED {
let now: BTreeSet<String> =
by_eff.get(eff).map(|m| m.keys().cloned().collect()).unwrap_or_default();
let was = base_layers.get(eff).cloned().unwrap_or_default();
for l in now.difference(&was) {
leaks.push(format!("{eff} → {l}"));
}
for l in was.difference(&now) {
cleanups.push(format!("{eff} ⊘ {l}"));
}
}
leaks.sort();
cleanups.sort();
if want_json {
let out = serde_json::json!({ "leaks": leaks, "cleanups": cleanups });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return if leaks.is_empty() { 0 } else { 1 };
}
if !leaks.is_empty() {
println!("[AS-EFF-010] a boundary effect leaked into a layer it wasn't in:");
for l in &leaks {
println!(" {l}");
}
}
if !cleanups.is_empty() {
if !leaks.is_empty() {
println!();
}
println!("✓ improved — a boundary effect left a layer:");
for c in &cleanups {
println!(" {c}");
}
}
if leaks.is_empty() && cleanups.is_empty() {
println!("candor containment: unchanged vs {base_pre} (no leaks, no cleanups).");
} else if leaks.is_empty() {
println!("\ncandor containment: no regressions ✓");
}
if !leaks.is_empty() {
println!("\nfix: keep the call in its boundary layer, or refresh the baseline if intended.");
}
return if leaks.is_empty() { 0 } else { 1 };
}
let owner_of = |layers: &BTreeMap<String, usize>| -> (String, usize) {
layers.iter().max_by_key(|(_, n)| **n).map(|(k, n)| (k.clone(), *n)).unwrap()
};
if want_json {
let contained: Vec<serde_json::Value> = CONTAINED
.iter()
.filter_map(|eff| {
by_eff.get(eff).map(|layers| {
let tot: usize = layers.values().sum();
let (owner, on) = owner_of(layers);
serde_json::json!({
"effect": eff, "containmentPct": 100 * on / tot,
"layers": layers.len(), "owner": owner, "placement": layers,
})
})
})
.collect();
let ambient: BTreeMap<&str, usize> =
AMBIENT.iter().filter_map(|e| by_eff.get(e).map(|m| (*e, m.len()))).collect();
let out = serde_json::json!({ "contained": contained, "ambient": ambient });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
println!("candor containment — how well each boundary effect stays in one layer");
println!("(the signal is dispersion across layers, NOT effect count)\n");
println!(" {:<7} {:>9} {:>7} owner ← leaked into", "effect", "contained", "layers");
let mut any = false;
for eff in CONTAINED {
let Some(layers) = by_eff.get(eff) else { continue };
any = true;
let tot: usize = layers.values().sum();
let (owner, on) = owner_of(layers);
let mut others: Vec<(&String, &usize)> = layers.iter().filter(|(k, _)| **k != owner).collect();
others.sort_by(|a, b| b.1.cmp(a.1));
let leaks: String =
others.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(", ");
let tail = if leaks.is_empty() { String::new() } else { format!(" ← {leaks}") };
println!(" {eff:<7} {:>8}% {:>7} {owner} ({on}){tail}", 100 * on / tot, layers.len());
}
if !any {
println!(" (no boundary effects in the report)");
}
let amb: String = AMBIENT
.iter()
.filter_map(|e| by_eff.get(e).map(|m| format!("{e} {}L", m.len())))
.collect::<Vec<_>>()
.join(", ");
if !amb.is_empty() {
println!("\n ambient (cross-cutting expected, not scored): {amb}");
}
println!(
"\n containment% = share of an effect's direct uses in its dominant layer; 100% = fully contained.\
\n ratchet a baseline: candor-query containment <prefix> <baseline_prefix> (exit 1 on a new leak)."
);
0
}
fn cmd_receipt(args: &[String]) -> i32 {
let Some(pre) = args.first().map(String::as_str) else {
eprintln!("usage: candor-query receipt <prefix>");
return 2;
};
let base = prefix_base(pre);
let fns = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let (tally, unresolved) = tally_effects(&fns);
let unresolved = unresolved.len();
const ORDER: [&str; 10] =
["Db", "Net", "Fs", "Exec", "Env", "Clock", "Ipc", "Rand", "Clipboard", "Log"];
debug_assert_eq!(
ORDER.iter().copied().collect::<BTreeSet<_>>(),
EFFECTS.iter().copied().collect::<BTreeSet<_>>(),
"receipt ORDER must be a permutation of candor_report::EFFECTS",
);
let effects = ORDER
.iter()
.filter(|k| tally.get(**k).copied().unwrap_or(0) > 0)
.map(|k| format!("{} {k}", tally[*k]))
.collect::<Vec<_>>()
.join(", ");
let (mut calib_c, calib_p, calib_path) = load_calibrated(pre, &base);
calib_c.extend(calib_path); let encountered = encountered_set(pre);
let join = |s: &BTreeSet<String>| s.iter().cloned().collect::<Vec<_>>().join(" ");
println!("fns\t{}", fns.len());
println!("effects\t{effects}");
println!("unresolved\t{unresolved}");
println!("calibrated\t{}|{}", join(&calib_c), join(&calib_p));
println!("encountered\t{}", join(&encountered));
0
}
fn cmd_gains(args: &[String]) -> i32 {
let (cur_pre, base_pre) = match args {
[a, b, ..] => (a.as_str(), b.as_str()),
_ => {
eprintln!("usage: candor-query gains <cur_prefix> <base_prefix> [--json]");
return 2;
}
};
for (which, pre) in [("current", cur_pre), ("baseline", base_pre)] {
if glob_reports(pre).is_empty() {
eprintln!("candor: no report files at {which} prefix `{pre}` — check the path.");
return 2;
}
}
let cur = load_fninfo(cur_pre);
let base = load_fninfo(base_pre);
let empty = BTreeSet::new();
let mut out: Vec<(String, String)> = Vec::new();
for (func, info) in &cur {
let b = base.get(func).map(|i| &i.inferred).unwrap_or(&empty);
for e in gained_effects(&info.inferred, b) {
out.push((func.clone(), e));
}
}
out.sort();
if args.iter().any(|a| a == "--json" || a == "1") {
let mut gained: Vec<&str> = out.iter().map(|(_, e)| e.as_str()).collect();
gained.sort_unstable();
gained.dedup();
let by_function: Vec<_> = out
.iter()
.map(|(f, e)| serde_json::json!({ "fn": f, "effect": e }))
.collect();
let v = serde_json::json!({ "gained": gained, "byFunction": by_function });
println!("{}", serde_json::to_string_pretty(&v).unwrap());
return 0;
}
for (func, e) in out {
println!("{func}\t{e}");
}
0
}
fn cmd_state(args: &[String]) -> i32 {
let root = args.first().map(String::as_str).unwrap_or(".");
let mut files: Vec<PathBuf> = Vec::new();
collect_rs(Path::new(root), &mut files);
files.sort();
let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut feed = |bytes: &[u8]| {
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
};
for f in &files {
let rel = f.strip_prefix(root).unwrap_or(f);
feed(rel.to_string_lossy().as_bytes());
feed(&[0]);
if let Ok(bytes) = std::fs::read(f) {
feed(&bytes);
}
feed(&[0]);
}
println!("{h:016x}");
0
}
fn report_backend(prefix: &str) -> &'static str {
let files = report_files(prefix);
if files.is_empty() {
"none"
} else if files.iter().any(|f| f.kind == "scan") {
"scan"
} else {
"lint"
}
}
fn is_scan_artifact(base: &str, name: &str) -> bool {
name.strip_prefix(base)
.and_then(|r| r.strip_prefix('.'))
.is_some_and(|rest| rest.contains(".scan.") || rest.ends_with(".scan.json"))
}
fn clear_other_reports(prefix: &str, keep: &str) -> usize {
let p = Path::new(prefix);
let dir = p.parent().filter(|d| !d.as_os_str().is_empty()).map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
let Some(base) = p.file_name().and_then(|s| s.to_str()) else { return 0 };
let prefix_dot = format!("{base}.");
let Ok(rd) = std::fs::read_dir(&dir) else { return 0 };
let mut removed = 0;
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(&prefix_dot) || !name.ends_with(".json") {
continue;
}
let scan = is_scan_artifact(base, name);
let remove = match keep {
"scan" => !scan, "lint" => scan, _ => false,
};
if remove && std::fs::remove_file(ent.path()).is_ok() {
removed += 1;
}
}
removed
}
fn cmd_reports(args: &[String]) -> i32 {
let exists_only = args.iter().any(|a| a == "--exists");
let backend = args.iter().any(|a| a == "--backend");
let clear_keep = args.iter().position(|a| a == "--clear-other").and_then(|i| args.get(i + 1)).cloned();
let Some(prefix) = args.iter().find(|a| !a.starts_with("--")) else {
eprintln!("usage: candor-query reports <prefix> [--exists | --backend | --clear-other <scan|lint>]");
return 2;
};
if backend {
println!("{}", report_backend(prefix));
return 0;
}
if let Some(keep) = clear_keep {
clear_other_reports(prefix, &keep);
return 0;
}
let files = report_files(prefix);
if exists_only {
return if files.is_empty() { 1 } else { 0 };
}
for rf in &files {
println!("{}", rf.path.display());
}
0
}
fn locate_newest(kind: &str, dirs: &[String]) -> Option<PathBuf> {
let matches_kind = |name: &str| -> bool {
match kind {
"scan" => name == "candor-scan",
"lib" => name.starts_with("libcandor@") && (name.ends_with(".dylib") || name.ends_with(".so")),
_ => false,
}
};
let mut newest: Option<(std::time::SystemTime, PathBuf)> = None;
for dir in dirs {
let Ok(rd) = std::fs::read_dir(dir) else { continue };
for ent in rd.flatten() {
let name = ent.file_name();
let Some(name) = name.to_str() else { continue };
if !matches_kind(name) {
continue;
}
let Ok(mtime) = ent.metadata().and_then(|m| m.modified()) else { continue };
if newest.as_ref().is_none_or(|(t, _)| mtime > *t) {
newest = Some((mtime, ent.path()));
}
}
}
newest.map(|(_, p)| p)
}
fn cmd_locate(args: &[String]) -> i32 {
let Some(kind) = args.first() else {
eprintln!("usage: candor-query locate <lib|scan> <dir>...");
return 2;
};
match locate_newest(kind, &args[1..]) {
Some(p) => {
println!("{}", p.display());
0
}
None => 1,
}
}
fn cmd_engine_version(args: &[String]) -> i32 {
let Some(path) = args.first() else {
eprintln!("usage: candor-query engine-version <lib-path>");
return 2;
};
let Ok(bytes) = std::fs::read(path) else { return 1 };
let needle = b"candor-build-version=";
let Some(start) = bytes.windows(needle.len()).position(|w| w == needle) else { return 1 };
let v: Vec<u8> = bytes[start + needle.len()..]
.iter()
.copied()
.take_while(|b| b.is_ascii_alphanumeric())
.collect();
if v.is_empty() {
return 1;
}
println!("{}", String::from_utf8_lossy(&v));
0
}
fn cmd_merge_hook(args: &[String]) -> i32 {
let (Some(path), Some(cmd)) = (args.first(), args.get(1)) else {
eprintln!("usage: candor-query merge-hook <settings.json> <hook-command>");
return 2;
};
let manual = || {
eprintln!(" WARNING: {path} isn't plain JSON (comments or trailing commas?) — NOT modifying it.");
eprintln!(
" Add this Stop hook by hand: {{\"matcher\":\"*\",\"hooks\":[{{\"type\":\"command\",\"command\":\"{cmd}\"}}]}}"
);
};
let mut data: serde_json::Value = if Path::new(path).exists() {
match std::fs::read_to_string(path) {
Ok(s) => match serde_json::from_str(&s) {
Ok(v) => v,
Err(_) => {
manual();
return 0; }
},
Err(e) => {
eprintln!("candor-query: cannot read {path}: {e}");
return 1;
}
}
} else {
serde_json::json!({})
};
let Some(obj) = data.as_object_mut() else {
manual();
return 0;
};
let hooks = obj.entry("hooks").or_insert_with(|| serde_json::json!({}));
let Some(stop) = hooks
.as_object_mut()
.map(|h| h.entry("Stop").or_insert_with(|| serde_json::json!([])))
.and_then(|s| s.as_array_mut())
else {
manual();
return 0;
};
let present = stop.iter().any(|g| {
g.get("hooks").and_then(|h| h.as_array()).is_some_and(|hs| {
hs.iter().any(|h| h.get("command").and_then(|c| c.as_str()) == Some(cmd.as_str()))
})
});
if present {
println!(" Stop hook already present in {path}");
return 0;
}
stop.push(serde_json::json!({"matcher": "*", "hooks": [{"type": "command", "command": cmd}]}));
let body = serde_json::to_string_pretty(&data).unwrap_or_default();
if let Err(e) = std::fs::write(path, format!("{body}\n")) {
eprintln!("candor-query: cannot write {path}: {e}");
return 1;
}
println!(" merged Stop hook into {path}");
0
}
fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for ent in rd.flatten() {
let path = ent.path();
let Ok(ft) = ent.file_type() else { continue };
if ft.is_dir() {
let name = ent.file_name();
if name == "target" || name == ".git" {
continue;
}
collect_rs(&path, out);
} else if ft.is_file() && path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
fn sorted(v: &[String]) -> Vec<String> {
let mut out = v.to_vec();
out.sort();
out
}
fn q_or(s: &str) -> &str {
if s.is_empty() { "?" } else { s }
}
fn three(args: &[String]) -> Option<(&str, &str, bool)> {
match args {
[a, b, c, ..] => Some((a.as_str(), b.as_str(), c == "1")),
_ => None,
}
}
fn two(args: &[String]) -> Option<(&str, bool)> {
match args {
[a, b, ..] => Some((a.as_str(), b == "1")),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_agents_contract_matches_the_repo_doc() {
let embedded = include_str!("../AGENTS.md");
match std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../AGENTS.md")) {
Ok(root) if root.contains("instructions for an AI coding agent") => {
assert_eq!(embedded, root, "crate AGENTS.md drifted from the repo root — re-copy it");
}
_ => { }
}
}
#[test]
fn rewire_flags_dropped_edges_not_added_ones() {
let cg = |pairs: &[(&str, &[&str])]| -> BTreeMap<String, Vec<String>> {
pairs.iter().map(|(k, v)| (k.to_string(), v.iter().map(|s| s.to_string()).collect())).collect()
};
let base = cg(&[("api::handle", &["service::place_order"]), ("cart::total", &["pricing::quote"])]);
let cur = cg(&[("cart::total", &["pricing::quote"]), ("main", &["api::handle"])]);
let d = dropped_edges(&cur, &base);
assert_eq!(d.get("api::handle"), Some(&vec!["service::place_order"])); assert!(!d.contains_key("cart::total")); assert!(!d.contains_key("main")); assert!(dropped_edges(&base, &base).is_empty());
}
#[test]
fn gains_flags_a_supply_chain_capability_gain() {
let set = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<BTreeSet<String>>();
assert_eq!(gained_effects(&set(&["Fs", "Net"]), &set(&["Fs"])), vec!["Net"]); assert!(gained_effects(&set(&["Fs"]), &set(&["Fs"])).is_empty()); assert!(gained_effects(&set(&["Fs"]), &set(&["Fs", "Net"])).is_empty()); }
#[test]
fn match_tier_ladder() {
assert_eq!(match_tier("pricing::Pricing::quote", "pricing::Pricing::quote"), 3);
assert_eq!(match_tier("pricing::Pricing::quote", "Pricing::quote"), 2);
assert_eq!(match_tier("pricing::Pricing::quote", "quote"), 2);
assert_eq!(match_tier("pricing::Pricing::quote_bulk", "quote"), 1); assert_eq!(match_tier("pricing::Pricing::quote", "ricing::quote"), 1); assert_eq!(match_tier("pricing::Pricing::quote", "zzz"), 0);
let names = ["pricing::Pricing::quote", "pricing::Pricing::quote_bulk"];
let t = best_tier(names.iter().copied(), "Pricing::quote");
assert_eq!(t, 2);
assert!(q_match(names[0], "Pricing::quote", t));
assert!(!q_match(names[1], "Pricing::quote", t));
let t2 = best_tier(names.iter().copied(), "ricing");
assert_eq!(t2, 1);
assert!(q_match(names[0], "ricing", t2) && q_match(names[1], "ricing", t2));
}
#[test]
fn whatif_scope_and_policy_parse() {
use candor_classify::policy::{parse_policy, scope_matches};
assert!(scope_matches("app::domain::handle", "domain"));
assert!(scope_matches("crate::domain_logic", "domain")); assert!(!scope_matches("app::subdomain::handle", "domain")); assert!(scope_matches("api::handle", "api"));
let p = parse_policy("deny Net Db api\npure parse\ndeny Exec\nallow Net in billing x\nforbid a -> b\n# c");
let rules = &p.rules;
assert_eq!(rules.len(), 3);
assert_eq!(rules[0].effects.iter().copied().collect::<Vec<_>>(), vec!["Db", "Net"]);
assert_eq!(rules[0].scope.as_deref(), Some("api"));
assert!(rules[1].effects.is_empty()); assert_eq!(rules[1].scope.as_deref(), Some("parse"));
assert_eq!(rules[2].scope, None); assert_eq!(p.allow_rules.len(), 1);
assert_eq!(p.layer_rules.len(), 1);
}
#[test]
fn containment_layer_derivation() {
let names: Vec<String> = ["pgman::conn::connect", "pgman::query::run", "pgman::main"]
.iter()
.map(|s| s.to_string())
.collect();
let refs: Vec<&String> = names.iter().collect();
let pl = common_prefix_len(&refs);
assert_eq!(pl, 1, "shared root is `pgman`");
assert_eq!(layer_of("pgman::conn::connect", pl), "conn");
assert_eq!(layer_of("pgman::query::Q::run", pl), "query");
assert_eq!(layer_of("pgman::main", pl), "(root)");
assert_eq!(norm_name("<pgman::conn::Conn as std::fmt::Debug>::fmt"), "pgman::conn::Conn");
assert_eq!(layer_of("<pgman::conn::Conn as std::fmt::Debug>::fmt", pl), "conn");
let multi: Vec<String> =
["a::x::f".to_string(), "b::y::g".to_string()].to_vec();
let mrefs: Vec<&String> = multi.iter().collect();
assert_eq!(common_prefix_len(&mrefs), 0);
assert_eq!(layer_of("a::x::f", 0), "a");
}
#[test]
fn is_scan_artifact_discriminates() {
assert!(is_scan_artifact("report", "report.mycrate.scan.json"));
assert!(is_scan_artifact("report", "report.mycrate.scan.callgraph.json"));
assert!(!is_scan_artifact("report", "report.mycrate.Rlib.json"));
assert!(!is_scan_artifact("report", "report.mycrate.Executable.callgraph.json"));
assert!(!is_scan_artifact("report", "report.calibrated.json"));
assert!(!is_scan_artifact("report", "other.mycrate.scan.json"));
}
#[test]
fn report_backend_and_clear_other() {
let dir = std::env::temp_dir().join("candor-query-backend-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let pre = dir.join("report");
let prefix = pre.to_string_lossy().to_string();
let w = |name: &str| std::fs::write(dir.join(name), b"{}").unwrap();
assert_eq!(report_backend(&prefix), "none");
w("report.c.scan.json");
w("report.c.scan.callgraph.json");
assert_eq!(report_backend(&prefix), "scan");
w("report.c.Rlib.json");
w("report.c.Rlib.callgraph.json");
w("report.calibrated.json");
assert_eq!(report_backend(&prefix), "scan"); let removed = clear_other_reports(&prefix, "lint"); assert_eq!(removed, 2); assert!(!dir.join("report.c.scan.json").exists());
assert!(dir.join("report.c.Rlib.json").exists());
assert_eq!(report_backend(&prefix), "lint");
w("report.c.scan.json");
let removed = clear_other_reports(&prefix, "scan");
assert!(removed >= 3); assert!(dir.join("report.c.scan.json").exists());
assert_eq!(report_backend(&prefix), "scan");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn locate_picks_newest_by_mtime() {
use std::time::{Duration, SystemTime};
let dir = std::env::temp_dir().join("candor-query-locate-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let old = dir.join("libcandor@nightly-2025-01-01-x.dylib");
let new = dir.join("libcandor@nightly-2026-01-01-x.dylib");
std::fs::write(&old, b"x").unwrap();
std::fs::write(&new, b"x").unwrap();
let f_old = std::fs::OpenOptions::new().write(true).open(&old).unwrap();
f_old.set_modified(SystemTime::now() - Duration::from_secs(100)).unwrap();
let f_new = std::fs::OpenOptions::new().write(true).open(&new).unwrap();
f_new.set_modified(SystemTime::now()).unwrap();
let out = locate_newest("lib", &[dir.to_string_lossy().to_string()]);
assert_eq!(out, Some(new)); let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn merge_hook_is_nondestructive_and_idempotent() {
let dir = std::env::temp_dir().join("candor-query-merge-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let cmd = "X/stop-hook.sh".to_string();
let arg = |p: &std::path::Path| vec![p.to_string_lossy().to_string(), cmd.clone()];
let fresh = dir.join("fresh.json");
assert_eq!(cmd_merge_hook(&arg(&fresh)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&fresh).unwrap()).unwrap();
assert_eq!(v["hooks"]["Stop"][0]["hooks"][0]["command"], cmd.as_str());
let keep = dir.join("keep.json");
std::fs::write(&keep, r#"{"model":"opus","permissions":{"allow":["Bash"]}}"#).unwrap();
assert_eq!(cmd_merge_hook(&arg(&keep)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&keep).unwrap()).unwrap();
assert_eq!(v["model"], "opus");
assert_eq!(v["permissions"]["allow"][0], "Bash");
assert_eq!(v["hooks"]["Stop"][0]["hooks"][0]["command"], cmd.as_str());
assert_eq!(cmd_merge_hook(&arg(&keep)), 0);
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&keep).unwrap()).unwrap();
assert_eq!(v["hooks"]["Stop"].as_array().unwrap().len(), 1);
let bad = dir.join("bad.json");
let original = "{ // comment\n \"model\": \"x\",\n}";
std::fs::write(&bad, original).unwrap();
assert_eq!(cmd_merge_hook(&arg(&bad)), 0);
assert_eq!(std::fs::read_to_string(&bad).unwrap(), original, "must not touch a non-JSON file");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn state_hash_is_deterministic_and_scoped() {
let dir = std::env::temp_dir().join("candor-query-state-test");
let _ = std::fs::remove_dir_all(&dir);
for d in ["src", "sub", "target/x", ".git"] {
std::fs::create_dir_all(dir.join(d)).unwrap();
}
std::fs::write(dir.join("src/a.rs"), "fn a(){}").unwrap();
std::fs::write(dir.join("sub/b.rs"), "fn b(){}").unwrap();
std::fs::write(dir.join("target/x/c.rs"), "fn c(){}").unwrap(); std::fs::write(dir.join(".git/d.rs"), "fn d(){}").unwrap(); std::fs::write(dir.join("src/notrust.txt"), "ignored").unwrap();
let hash = |root: &Path| -> u64 {
let mut files = Vec::new();
collect_rs(root, &mut files);
files.sort();
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for f in &files {
for &b in f.strip_prefix(root).unwrap_or(f).to_string_lossy().as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
for &b in std::fs::read(f).unwrap_or_default().iter() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
};
let mut files = Vec::new();
collect_rs(&dir, &mut files);
assert_eq!(files.len(), 2, "must collect exactly src/a.rs + sub/b.rs");
let h1 = hash(&dir);
assert_eq!(h1, hash(&dir), "deterministic");
std::fs::write(dir.join("target/x/c.rs"), "fn c(){ let _=9; }").unwrap();
std::fs::write(dir.join(".git/d.rs"), "fn d(){ let _=9; }").unwrap();
assert_eq!(h1, hash(&dir), "target/ and .git/ edits are ignored");
std::fs::write(dir.join("src/a.rs"), "fn a(){ let _=1; }").unwrap();
assert_ne!(h1, hash(&dir), "a real .rs edit changes the hash");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn globs_discriminate_reports_from_sidecars() {
let dir = std::env::temp_dir().join("candor-query-glob-test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for f in [
"report.mycrate.lib.json", "report.mycrate.Executable.json", "report.calibrated.json", "report.encountered-mycrate.json", "report.single.json", "other.a.b.json", ] {
std::fs::write(dir.join(f), "[]").unwrap();
}
let prefix = dir.join("report");
let prefix = prefix.to_str().unwrap();
let reports: Vec<String> =
glob_reports(prefix).iter().map(|p| p.file_name().unwrap().to_str().unwrap().to_string()).collect();
assert_eq!(reports, vec!["report.mycrate.Executable.json", "report.mycrate.lib.json"]);
let enc: Vec<String> =
glob_encountered(prefix).iter().map(|p| p.file_name().unwrap().to_str().unwrap().to_string()).collect();
assert_eq!(enc, vec!["report.encountered-mycrate.json"]);
assert_eq!(prefix_base(prefix), "report");
let _ = std::fs::remove_dir_all(&dir);
}
}