use crate::*;
pub(crate) fn glob_reports(prefix: &str) -> Vec<PathBuf> {
report_files(prefix).into_iter().map(|r| r.path).collect()
}
pub(crate) 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
}
pub(crate) 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))
}
pub(crate) fn prefix_base(prefix: &str) -> String {
Path::new(prefix).file_name().and_then(|s| s.to_str()).unwrap_or("").to_string()
}
pub(crate) 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
}
pub(crate) 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
}
pub(crate) 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"))
}
pub(crate) 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
}
pub(crate) 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
}
pub(crate) 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"
}
}
pub(crate) 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"))
}