1use std::collections::{BTreeMap, BTreeSet, HashSet};
5
6use serde::{Deserialize, Serialize};
7
8use crate::conventions::{Convention, Manual, ORG};
9use crate::graph::{CodeGraph, ContractView, Node};
10use crate::ledger::Entry;
11use crate::manifests::InternalDep;
12use crate::model::{Edge, Symbol};
13use crate::search::SearchHit;
14
15pub fn find(g: &CodeGraph, query: &str, repos: &[String]) -> Vec<Node> {
17 let mut hits = g.find(query, if repos.is_empty() { 30 } else { 200 });
19 if !repos.is_empty() {
20 hits.retain(|n| in_repos(&n.repo, repos));
21 hits.truncate(30);
22 }
23 hits
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Search {
28 pub hits: Vec<SearchHit>,
29 pub markdown: String,
30}
31
32pub fn search_limit(repos: &[String]) -> usize {
34 if repos.is_empty() {
35 12
36 } else {
37 60
38 }
39}
40
41pub fn search(query: &str, mut hits: Vec<SearchHit>, repos: &[String]) -> Search {
43 if !repos.is_empty() {
44 hits.retain(|h| in_repos(&h.repo, repos));
45 hits.truncate(12);
46 }
47 Search {
48 markdown: crate::search::markdown(query, &hits),
49 hits,
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Why {
55 pub name: String,
56 pub markdown: String,
57}
58
59pub fn why(g: &CodeGraph, entries: &[Entry], focus: &str) -> Option<Why> {
61 let (name, ids) = g.scope_of(focus)?;
62 let about = crate::ledger::about(entries, &ids);
63 Some(Why {
64 markdown: crate::ledger::markdown(&format!("why `{name}`"), &about),
65 name,
66 })
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct Ledger {
71 pub markdown: String,
72 pub note: Option<String>,
74 pub failed: bool,
76}
77
78pub fn ledger(
80 mut entries: Vec<Entry>,
81 symbols: &[Symbol],
82 edges: &[Edge],
83 packages: &[crate::manifests::PackageRow],
84 inbox: bool,
85 check: bool,
86) -> Ledger {
87 let findings = crate::checks::run(&entries, symbols, edges, packages);
88 if check {
89 if findings.is_empty() {
90 return Ledger {
91 note: Some("no constraint carries a check (add one with --field check=...)".into()),
92 ..Default::default()
93 };
94 }
95 return Ledger {
96 markdown: format!(
97 "## Constraint checks\n\n{}",
98 crate::checks::markdown(&findings, None)
99 ),
100 failed: findings
101 .iter()
102 .any(|f| f.severity == "must" && !f.violations.is_empty()),
103 note: None,
104 };
105 }
106 crate::checks::promote(&mut entries, &findings);
107 let shown = crate::ledger::inbox(&entries, inbox);
108 let title = if inbox { "Ledger inbox" } else { "Ledger" };
109 let mut markdown = format!("{}\n", crate::ledger::markdown(title, &shown));
110 let revived = crate::checks::revivals(&entries, packages);
111 if !revived.is_empty() {
112 markdown.push_str("## Retry conditions met\n\n");
113 for (e, why) in revived {
114 markdown.push_str(&format!("- {} — {why}\n", crate::ledger::line(e)));
115 }
116 }
117 Ledger {
118 markdown,
119 ..Default::default()
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Flow {
125 pub focus: String,
126 pub narration: Option<(String, Vec<String>)>,
128 pub steps: Vec<Step>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Step {
133 pub depth: usize,
134 pub via: String,
135 pub name: String,
136 pub location: String,
137}
138
139pub fn flow(
141 g: &CodeGraph,
142 focus: &str,
143 narration: impl FnOnce(&str) -> Option<(String, Vec<String>)>,
144) -> Option<Flow> {
145 let idx = g.lookup(focus)?;
146 let narration = g.zoom(idx).focus.and_then(|n| narration(&n.id));
147 let steps = g
148 .thread(idx, 4)
149 .into_iter()
150 .map(|h| Step {
151 depth: h.depth,
152 via: h.via.to_string(),
153 name: h.node.name,
154 location: h.node.location,
155 })
156 .collect();
157 Some(Flow {
158 focus: focus.to_string(),
159 narration,
160 steps,
161 })
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct Contracts {
166 pub total: usize,
168 pub views: Vec<ContractView>,
169}
170
171pub fn contracts(g: &CodeGraph, kind: Option<&str>, dangling: bool) -> Contracts {
172 Contracts {
173 total: g.contracts(None, false).len(),
174 views: g.contracts(kind, dangling),
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Conventions {
180 pub conventions: Vec<Convention>,
181 pub hidden: Vec<Manual>,
182}
183
184pub fn conventions(all: Vec<Convention>, manual: &[Manual], repo: Option<&str>) -> Conventions {
187 let here = |r: &str| repo.is_none_or(|want| r == want || r == ORG);
188 Conventions {
189 conventions: all.into_iter().filter(|c| here(&c.repo)).collect(),
190 hidden: manual
191 .iter()
192 .filter(|m| m.hidden && here(&m.repo))
193 .cloned()
194 .collect(),
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Explore {
200 pub symbols: usize,
201 pub edges: usize,
202 pub dangling: usize,
203 pub view: Option<ExploreView>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ExploreView {
209 pub focus: Option<Node>,
210 pub container: Option<Node>,
211 pub callees: Vec<Node>,
212 pub callers: Vec<Node>,
213 pub importers: Vec<Node>,
214 pub members: Vec<Node>,
215 pub dependents: usize,
217 pub by_repo: BTreeMap<String, usize>,
218 pub budget: usize,
219 pub pack: Vec<(Node, usize)>,
221}
222
223pub const RIPPLE_DEPTH: usize = 4;
225pub const PACK_BUDGET: usize = 1500;
226
227pub fn explore(g: &CodeGraph, focus: &str) -> Explore {
229 let view = g.lookup(focus).map(|idx| {
230 let z = g.zoom(idx);
231 let hits = g.ripple(idx, RIPPLE_DEPTH);
232 let mut by_repo = BTreeMap::new();
233 for h in &hits {
234 *by_repo.entry(h.node.repo.clone()).or_default() += 1;
235 }
236 ExploreView {
237 focus: z.focus,
238 container: z.container,
239 callees: z.callees,
240 callers: z.callers,
241 importers: z.importers,
242 members: z.members,
243 dependents: hits.len(),
244 by_repo,
245 budget: PACK_BUDGET,
246 pack: g.context_pack(idx, PACK_BUDGET),
247 }
248 });
249 Explore {
250 symbols: g.node_count(),
251 edges: g.edge_count(),
252 dangling: g.dangling_edges,
253 view,
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct Stats {
259 pub symbols: usize,
260 pub indexed: usize,
262 pub unknown: Vec<String>,
264 pub repos: Vec<RepoStats>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct RepoStats {
269 pub repo: String,
270 pub symbols: usize,
271 pub edges: usize,
272 pub enriched: usize,
273 pub kinds: BTreeMap<String, usize>,
274}
275
276pub fn stats(
278 symbols: &[Symbol],
279 edges: &[Edge],
280 enriched: &HashSet<String>,
281 want: &[String],
282) -> Stats {
283 let all: BTreeSet<&str> = symbols.iter().map(|s| s.id.package.as_str()).collect();
284 let (known, unknown): (Vec<&String>, Vec<&String>) =
285 want.iter().partition(|r| all.contains(r.as_str()));
286 let repos: Vec<&str> = if want.is_empty() {
287 all.iter().copied().collect()
288 } else {
289 known.into_iter().map(String::as_str).collect()
290 };
291 let repos = repos
292 .into_iter()
293 .map(|repo| {
294 let rsyms: Vec<&Symbol> = symbols.iter().filter(|s| s.id.package == repo).collect();
295 let mut kinds = BTreeMap::new();
296 for s in &rsyms {
297 *kinds.entry(format!("{:?}", s.kind)).or_default() += 1;
298 }
299 RepoStats {
300 repo: repo.to_string(),
301 symbols: rsyms.len(),
302 edges: edges.iter().filter(|e| e.src.package == repo).count(),
303 enriched: rsyms
304 .iter()
305 .filter(|s| enriched.contains(&s.id.render()))
306 .count(),
307 kinds,
308 }
309 })
310 .collect();
311 Stats {
312 symbols: symbols.len(),
313 indexed: all.len(),
314 unknown: unknown.into_iter().cloned().collect(),
315 repos,
316 }
317}
318
319pub fn packages(deps: Vec<InternalDep>, skew: bool) -> Vec<InternalDep> {
321 deps.into_iter().filter(|d| !skew || d.skewed()).collect()
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Preflight {
326 pub markdown: String,
327 pub blocking: bool,
329}
330
331pub fn preflight(
333 g: &CodeGraph,
334 symbols: &[Symbol],
335 edges: &[Edge],
336 mut entries: Vec<Entry>,
337 recipes: &[Convention],
338 packages: &[crate::manifests::PackageRow],
339 changes: &[crate::preflight::Change],
340) -> Preflight {
341 let findings = crate::checks::run(&entries, symbols, edges, packages);
342 crate::checks::promote(&mut entries, &findings);
343 let markdown =
344 crate::preflight::report(g, symbols, edges, &entries, recipes, &findings, changes);
345 let changed: Vec<String> = changes
346 .iter()
347 .map(|c| format!("{}/{}", c.repo, c.path))
348 .collect();
349 let blocking = findings.iter().any(|f| {
350 f.severity == "must"
351 && f.violations
352 .iter()
353 .any(|v| changed.contains(&format!("{}/{}", v.repo, v.path)))
354 });
355 Preflight { markdown, blocking }
356}
357
358fn in_repos(repo: &str, repos: &[String]) -> bool {
359 repos.iter().any(|r| repo.eq_ignore_ascii_case(r))
360}