use std::collections::{BTreeMap, BTreeSet, HashSet};
use serde::{Deserialize, Serialize};
use crate::conventions::{Convention, Manual, ORG};
use crate::graph::{CodeGraph, ContractView, Node};
use crate::ledger::Entry;
use crate::manifests::InternalDep;
use crate::model::{Edge, Symbol};
use crate::search::SearchHit;
pub fn find(g: &CodeGraph, query: &str, repos: &[String]) -> Vec<Node> {
let mut hits = g.find(query, if repos.is_empty() { 30 } else { 200 });
if !repos.is_empty() {
hits.retain(|n| in_repos(&n.repo, repos));
hits.truncate(30);
}
hits
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Search {
pub hits: Vec<SearchHit>,
pub markdown: String,
}
pub fn search_limit(repos: &[String]) -> usize {
if repos.is_empty() {
12
} else {
60
}
}
pub fn search(query: &str, mut hits: Vec<SearchHit>, repos: &[String]) -> Search {
if !repos.is_empty() {
hits.retain(|h| in_repos(&h.repo, repos));
hits.truncate(12);
}
Search {
markdown: crate::search::markdown(query, &hits),
hits,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Why {
pub name: String,
pub markdown: String,
}
pub fn why(g: &CodeGraph, entries: &[Entry], focus: &str) -> Option<Why> {
let (name, ids) = g.scope_of(focus)?;
let about = crate::ledger::about(entries, &ids);
Some(Why {
markdown: crate::ledger::markdown(&format!("why `{name}`"), &about),
name,
})
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Ledger {
pub markdown: String,
pub note: Option<String>,
pub failed: bool,
}
pub fn ledger(
mut entries: Vec<Entry>,
symbols: &[Symbol],
edges: &[Edge],
packages: &[crate::manifests::PackageRow],
inbox: bool,
check: bool,
) -> Ledger {
let findings = crate::checks::run(&entries, symbols, edges, packages);
if check {
if findings.is_empty() {
return Ledger {
note: Some("no constraint carries a check (add one with --field check=...)".into()),
..Default::default()
};
}
return Ledger {
markdown: format!(
"## Constraint checks\n\n{}",
crate::checks::markdown(&findings, None)
),
failed: findings
.iter()
.any(|f| f.severity == "must" && !f.violations.is_empty()),
note: None,
};
}
crate::checks::promote(&mut entries, &findings);
let shown = crate::ledger::inbox(&entries, inbox);
let title = if inbox { "Ledger inbox" } else { "Ledger" };
let mut markdown = format!("{}\n", crate::ledger::markdown(title, &shown));
let revived = crate::checks::revivals(&entries, packages);
if !revived.is_empty() {
markdown.push_str("## Retry conditions met\n\n");
for (e, why) in revived {
markdown.push_str(&format!("- {} — {why}\n", crate::ledger::line(e)));
}
}
Ledger {
markdown,
..Default::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Flow {
pub focus: String,
pub narration: Option<(String, Vec<String>)>,
pub steps: Vec<Step>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Step {
pub depth: usize,
pub via: String,
pub name: String,
pub location: String,
}
pub fn flow(
g: &CodeGraph,
focus: &str,
narration: impl FnOnce(&str) -> Option<(String, Vec<String>)>,
) -> Option<Flow> {
let idx = g.lookup(focus)?;
let narration = g.zoom(idx).focus.and_then(|n| narration(&n.id));
let steps = g
.thread(idx, 4)
.into_iter()
.map(|h| Step {
depth: h.depth,
via: h.via.to_string(),
name: h.node.name,
location: h.node.location,
})
.collect();
Some(Flow {
focus: focus.to_string(),
narration,
steps,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contracts {
pub total: usize,
pub views: Vec<ContractView>,
}
pub fn contracts(g: &CodeGraph, kind: Option<&str>, dangling: bool) -> Contracts {
Contracts {
total: g.contracts(None, false).len(),
views: g.contracts(kind, dangling),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conventions {
pub conventions: Vec<Convention>,
pub hidden: Vec<Manual>,
}
pub fn conventions(all: Vec<Convention>, manual: &[Manual], repo: Option<&str>) -> Conventions {
let here = |r: &str| repo.is_none_or(|want| r == want || r == ORG);
Conventions {
conventions: all.into_iter().filter(|c| here(&c.repo)).collect(),
hidden: manual
.iter()
.filter(|m| m.hidden && here(&m.repo))
.cloned()
.collect(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Explore {
pub symbols: usize,
pub edges: usize,
pub dangling: usize,
pub view: Option<ExploreView>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExploreView {
pub focus: Option<Node>,
pub container: Option<Node>,
pub callees: Vec<Node>,
pub callers: Vec<Node>,
pub importers: Vec<Node>,
pub members: Vec<Node>,
pub dependents: usize,
pub by_repo: BTreeMap<String, usize>,
pub budget: usize,
pub pack: Vec<(Node, usize)>,
}
pub const RIPPLE_DEPTH: usize = 4;
pub const PACK_BUDGET: usize = 1500;
pub fn explore(g: &CodeGraph, focus: &str) -> Explore {
let view = g.lookup(focus).map(|idx| {
let z = g.zoom(idx);
let hits = g.ripple(idx, RIPPLE_DEPTH);
let mut by_repo = BTreeMap::new();
for h in &hits {
*by_repo.entry(h.node.repo.clone()).or_default() += 1;
}
ExploreView {
focus: z.focus,
container: z.container,
callees: z.callees,
callers: z.callers,
importers: z.importers,
members: z.members,
dependents: hits.len(),
by_repo,
budget: PACK_BUDGET,
pack: g.context_pack(idx, PACK_BUDGET),
}
});
Explore {
symbols: g.node_count(),
edges: g.edge_count(),
dangling: g.dangling_edges,
view,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stats {
pub symbols: usize,
pub indexed: usize,
pub unknown: Vec<String>,
pub repos: Vec<RepoStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoStats {
pub repo: String,
pub symbols: usize,
pub edges: usize,
pub enriched: usize,
pub kinds: BTreeMap<String, usize>,
}
pub fn stats(
symbols: &[Symbol],
edges: &[Edge],
enriched: &HashSet<String>,
want: &[String],
) -> Stats {
let all: BTreeSet<&str> = symbols.iter().map(|s| s.id.package.as_str()).collect();
let (known, unknown): (Vec<&String>, Vec<&String>) =
want.iter().partition(|r| all.contains(r.as_str()));
let repos: Vec<&str> = if want.is_empty() {
all.iter().copied().collect()
} else {
known.into_iter().map(String::as_str).collect()
};
let repos = repos
.into_iter()
.map(|repo| {
let rsyms: Vec<&Symbol> = symbols.iter().filter(|s| s.id.package == repo).collect();
let mut kinds = BTreeMap::new();
for s in &rsyms {
*kinds.entry(format!("{:?}", s.kind)).or_default() += 1;
}
RepoStats {
repo: repo.to_string(),
symbols: rsyms.len(),
edges: edges.iter().filter(|e| e.src.package == repo).count(),
enriched: rsyms
.iter()
.filter(|s| enriched.contains(&s.id.render()))
.count(),
kinds,
}
})
.collect();
Stats {
symbols: symbols.len(),
indexed: all.len(),
unknown: unknown.into_iter().cloned().collect(),
repos,
}
}
pub fn packages(deps: Vec<InternalDep>, skew: bool) -> Vec<InternalDep> {
deps.into_iter().filter(|d| !skew || d.skewed()).collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Preflight {
pub markdown: String,
pub blocking: bool,
}
pub fn preflight(
g: &CodeGraph,
symbols: &[Symbol],
edges: &[Edge],
mut entries: Vec<Entry>,
recipes: &[Convention],
packages: &[crate::manifests::PackageRow],
changes: &[crate::preflight::Change],
) -> Preflight {
let findings = crate::checks::run(&entries, symbols, edges, packages);
crate::checks::promote(&mut entries, &findings);
let markdown =
crate::preflight::report(g, symbols, edges, &entries, recipes, &findings, changes);
let changed: Vec<String> = changes
.iter()
.map(|c| format!("{}/{}", c.repo, c.path))
.collect();
let blocking = findings.iter().any(|f| {
f.severity == "must"
&& f.violations
.iter()
.any(|v| changed.contains(&format!("{}/{}", v.repo, v.path)))
});
Preflight { markdown, blocking }
}
fn in_repos(repo: &str, repos: &[String]) -> bool {
repos.iter().any(|r| repo.eq_ignore_ascii_case(r))
}