1#![forbid(unsafe_code)]
2use std::collections::HashSet;
34use rustc_hash::FxHashMap;
35use std::path::Path;
36
37use rusqlite::Connection;
38
39use reliary_sift::classify::aggressive_skeleton;
40
41#[derive(Debug, Clone, Copy)]
47pub enum PackFormat {
48 L2L3,
50 Full,
52 BodySnippets,
55}
56
57#[derive(Debug, Clone, Copy)]
64pub enum GateDecision {
65 Full,
67 Minimal,
69 Skip,
71}
72
73pub fn compute_complexity_score(path: &str) -> Result<f32, String> {
81 let db_path = format!(
82 "{}/.reliary/index.sqlite",
83 path.trim_end_matches('/')
84 );
85 let db = Connection::open(&db_path)
86 .map_err(|e| format!("cannot open index at {}: {}", db_path, e))?;
87 reliary_search::schema::open_existing_db_safe(&db)
88 .map_err(|e| format!("index not initialized: {}", e))?;
89 compute_complexity_score_for(&db, path)
91}
92
93pub fn gate_decision(score: f32) -> GateDecision {
107 const HIGH_THRESHOLD: f32 = 6.0;
108 const LOW_THRESHOLD: f32 = 4.0;
109
110 if score > HIGH_THRESHOLD {
111 GateDecision::Full
112 } else if score > LOW_THRESHOLD {
113 GateDecision::Minimal
114 } else {
115 GateDecision::Skip
116 }
117}
118
119pub fn should_inject_pack(path: &str) -> Result<GateDecision, String> {
121 let score = compute_complexity_score(path)?;
122 Ok(gate_decision(score))
123}
124
125pub fn gate_decision_for(conn: &Connection, path: &str) -> Result<GateDecision, String> {
127 let score = compute_complexity_score_for(conn, path)?;
128 Ok(gate_decision(score))
129}
130
131pub fn compute_complexity_score_for(conn: &Connection, _path: &str) -> Result<f32, String> {
134 let row: (i64, i64) = conn.query_row(
137 "SELECT
138 COUNT(DISTINCT CASE WHEN o.is_def = 1 AND (
139 f.file_path LIKE '%.rs' OR f.file_path LIKE '%.py' OR f.file_path LIKE '%.ts'
140 OR f.file_path LIKE '%.go' OR f.file_path LIKE '%.c' OR f.file_path LIKE '%.js'
141 OR f.file_path LIKE '%.java' OR f.file_path LIKE '%.rb' OR f.file_path LIKE '%.swift'
142 OR f.file_path LIKE '%.kt' OR f.file_path LIKE '%.zig' OR f.file_path LIKE '%.nim'
143 ) THEN p.phrase END),
144 COUNT(CASE WHEN o.is_def = 0 AND (
145 f.file_path NOT LIKE '%/bench/%' AND f.file_path NOT LIKE '%/tests/%'
146 AND f.file_path NOT LIKE '%/test/%' AND f.file_path NOT LIKE '%/target/%'
147 AND f.file_path NOT LIKE '%/vendor/%' AND f.file_path NOT LIKE '%/node_modules/%'
148 AND f.file_path NOT LIKE '%/__pycache__/%' AND f.file_path NOT LIKE '%/.specstory/%'
149 ) THEN 1 END)
150 FROM occurrence o
151 JOIN phrases p ON o.phrase_id = p.id
152 JOIN file_map f ON o.file_id = f.id",
153 [], |r| Ok((r.get(0)?, r.get(1)?)),
154 ).unwrap_or((0, 0));
155
156 let (symbol_count, non_def_count) = row;
157 if symbol_count == 0 {
158 return Ok(0.0);
159 }
160 let avg_block_span: f32 = conn.query_row(
162 "SELECT AVG(CAST(end_line - start_line AS REAL))
163 FROM block WHERE end_line > start_line",
164 [], |r| r.get::<_, Option<f64>>(0),
165 ).ok().flatten().unwrap_or(0.0) as f32;
166
167 let specific_count: i64 = conn.query_row(
168 "SELECT COUNT(DISTINCT p.phrase)
169 FROM phrases p
170 WHERE length(p.phrase) > 8
171 AND (p.phrase LIKE '%\\_%' ESCAPE '\\' OR substr(p.phrase, 2, 1) GLOB '[a-z]'
172 AND substr(p.phrase, 1, 1) GLOB '[A-Z]')",
173 [], |r| r.get(0),
174 ).unwrap_or(0);
175 let total_phrases: i64 = conn.query_row("SELECT COUNT(*) FROM phrases", [], |r| r.get(0)).unwrap_or(1);
176 let specificity_ratio = if total_phrases > 0 { specific_count as f32 / total_phrases as f32 } else { 0.0 };
177
178 let log_symbols = (symbol_count.max(1) as f32).ln();
179 let log_xrefs = (non_def_count.max(1) as f32).ln();
180 let span_score = avg_block_span / 10.0;
181
182 Ok(0.25 * log_symbols + 0.20 * span_score + 0.25 * log_xrefs + 0.30 * specificity_ratio)
183}
184
185pub fn generate_pack(path: &str, format: PackFormat) -> Result<String, String> {
190 let db_path = format!(
191 "{}/.reliary/index.sqlite",
192 path.trim_end_matches('/')
193 );
194 let db = Connection::open(&db_path)
195 .map_err(|e| format!("cannot open index at {}: {}", db_path, e))?;
196
197 reliary_search::schema::open_existing_db_safe(&db)
199 .map_err(|e| format!("index not initialized: {}", e))?;
200
201 let has_any: i64 = db
205 .query_row("SELECT EXISTS(SELECT 1 FROM occurrence LIMIT 1)", [], |r| r.get(0))
206 .unwrap_or(1);
207 if has_any == 0 {
208 let _ = reliary_search::lazy_occurrence::build_all_occurrence(&db);
209 }
210
211 let symbols = extract_symbols_from_index(&db)?;
213
214 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
216
217 let cross_refs = build_cross_refs_from_index(&db, &symbols);
219
220 let mut entries = Vec::new();
222 entries.push(format!(
223 "# Holographic Pack — {} ({} symbols)\n",
224 Path::new(path)
225 .file_name()
226 .map(|n| n.to_string_lossy().to_string())
227 .unwrap_or_else(|| path.to_string()),
228 symbols.len()
229 ));
230
231 for sym in &symbols {
232 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
233 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
234 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
235 entries.push(entry);
236 }
237
238 Ok(entries.join("\n"))
239}
240
241pub fn generate_pack_hotspot(
249 path: &str,
250 format: PackFormat,
251 top_k: usize,
252) -> Result<String, String> {
253 let db = open_index(path)?;
254 generate_pack_hotspot_with(&db, path, format, top_k)
255}
256
257pub fn generate_pack_hotspot_with(
259 db: &Connection,
260 path: &str,
261 format: PackFormat,
262 top_k: usize,
263) -> Result<String, String> {
264 let symbols = extract_symbols_from_index(db)?;
265
266 let inbound_counts = compute_inbound_ref_counts(db, &symbols);
269 let mut scored: Vec<(f32, &Symbol)> = symbols
270 .iter()
271 .map(|s| {
272 let inbound = *inbound_counts.get(&s.name).unwrap_or(&0) as f32;
273 (0.6 * inbound, s) })
275 .collect();
276 let k = top_k.min(scored.len());
278 if k < scored.len() {
279 scored.select_nth_unstable_by(k, |a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
280 }
281 scored.truncate(k);
282
283 let selected: Vec<Symbol> = scored
284 .into_iter()
285 .map(|(_, s)| s.clone())
286 .collect();
287
288 let (symbol_sources, skeleton_freq) = read_symbols_for_subset(&selected, &symbols);
289
290 let cross_refs = build_cross_refs_for_subset(db, &selected, &symbols);
292
293 let mut entries = Vec::with_capacity(selected.len() + 1);
294 entries.push(format!(
295 "# Holographic Pack — {} ({} of {} symbols, hotspot-ranked)\n",
296 Path::new(path)
297 .file_name()
298 .map(|n| n.to_string_lossy().to_string())
299 .unwrap_or_else(|| path.to_string()),
300 selected.len(),
301 symbols.len()
302 ));
303
304 for sym in &selected {
305 let refs = cross_refs.get(&sym.name).map(|v| v.as_slice()).unwrap_or(&[]);
307 let source = symbol_sources.get(&sym.name).map(|s| s.as_str()).unwrap_or("");
308 let entry = render_entry(sym, source, refs, &skeleton_freq, format);
309 entries.push(entry);
310 }
311
312 let result = entries.join("\n");
313 Ok(result)
314}
315
316fn compute_inbound_ref_counts(
320 db: &Connection,
321 symbols: &[Symbol],
322) -> FxHashMap<String, usize> {
323 let mut counts: FxHashMap<String, usize> = FxHashMap::default();
324 let symbol_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
325
326 let mut stmt = match db.prepare(
329 "SELECT p.phrase, COUNT(*) as cnt
330 FROM occurrence o
331 JOIN phrases p ON o.phrase_id = p.id
332 JOIN file_map f ON o.file_id = f.id
333 WHERE o.is_def = 0
334 AND f.file_path NOT LIKE '%/bench/%'
335 AND f.file_path NOT LIKE '%/tests/%'
336 AND f.file_path NOT LIKE '%/test/%'
337 AND f.file_path NOT LIKE '%/target/%'
338 AND f.file_path NOT LIKE '%/vendor/%'
339 AND f.file_path NOT LIKE '%/node_modules/%'
340 AND f.file_path NOT LIKE '%/__pycache__/%'
341 AND f.file_path NOT LIKE '%/.specstory/%'
342 GROUP BY p.phrase",
343 ) {
344 Ok(s) => s,
345 Err(_) => return counts,
346 };
347
348 let rows = stmt.query_map([], |row| {
349 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
350 });
351
352 if let Ok(rows) = rows {
353 for row in rows.flatten() {
354 let (phrase, cnt) = row;
355 if symbol_names.contains(phrase.as_str()) {
357 counts.insert(phrase, cnt as usize);
358 }
359 }
360 }
361
362 counts
363}
364
365fn build_cross_refs_for_subset(
368 db: &Connection,
369 selected: &[Symbol],
370 all_symbols: &[Symbol],
371) -> FxHashMap<String, Vec<String>> {
372 let all_names: HashSet<&str> = all_symbols.iter().map(|s| s.name.as_str()).collect();
373 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
374
375 let mut all_pairs_stmt = match db.prepare(
382 "SELECT o.file_id, p.phrase, o.is_def
383 FROM occurrence o
384 JOIN phrases p ON o.phrase_id = p.id
385 WHERE p.phrase IN (SELECT phrase FROM phrases WHERE length(phrase) BETWEEN 3 AND 30)
386 ORDER BY o.file_id LIMIT 200000",
387 ) {
388 Ok(s) => s,
389 Err(_) => return cross_refs,
390 };
391
392 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
393 for row in all_pairs_stmt
394 .query_map([], |row| {
395 Ok((
396 row.get::<_, i64>(0)?,
397 row.get::<_, String>(1)?,
398 row.get::<_, i64>(2)? != 0,
399 ))
400 })
401 .ok()
402 .into_iter()
403 .flatten()
404 .filter_map(|r| r.ok())
405 {
406 let (file_id, phrase, is_def) = row;
407 file_to_phrases.entry(file_id).or_default().push((phrase, is_def));
408 }
409
410 for sym in selected {
413 let mut usage_files: Vec<i64> = file_to_phrases
418 .iter()
419 .filter(|(_, pairs)| pairs.iter().any(|(p, is_def)| *p == sym.name && !*is_def))
420 .map(|(fid, _)| *fid)
421 .collect();
422 usage_files.sort();
423 usage_files.truncate(50);
424
425 let mut refs: HashSet<String> = HashSet::new();
426 for file_id in usage_files {
427 if let Some(pairs) = file_to_phrases.get(&file_id) {
428 for (phrase, is_def) in pairs {
429 if *is_def
430 && *phrase != sym.name
431 && all_names.contains(phrase.as_str())
432 && !is_common_word(phrase)
433 {
434 refs.insert(phrase.clone());
435 }
436 if refs.len() >= 6 {
437 break;
438 }
439 }
440 }
441 if refs.len() >= 6 {
442 break;
443 }
444 }
445
446 let mut ordered: Vec<String> = refs.into_iter().collect();
451 ordered.sort();
452 ordered.truncate(6);
453 cross_refs.insert(sym.name.clone(), ordered);
454 }
455
456 cross_refs
457}
458
459pub fn generate_pack_auto(path: &str) -> Result<String, String> {
465 let conn = open_index(path)?;
467 let decision = gate_decision_for(&conn, path)?;
468
469 match decision {
470 GateDecision::Skip => Ok(String::new()),
471 GateDecision::Minimal => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 15),
472 GateDecision::Full => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 50),
473 }
474}
475
476fn open_index(codebase_path: &str) -> Result<rusqlite::Connection, String> {
478 let db_path = format!(
479 "{}/.reliary/index.sqlite",
480 codebase_path.trim_end_matches('/')
481 );
482 let conn = Connection::open(&db_path)
483 .map_err(|e| format!("pack: open {}: {}", db_path, e))?;
484 reliary_search::schema::open_existing_db_safe(&conn)
485 .map_err(|e| format!("index not initialized: {}", e))?;
486 Ok(conn)
487}
488
489#[derive(Debug, Clone)]
491pub struct PackEntry {
492 pub name: String,
493 pub header: String, pub body: String, }
496
497pub fn parse_pack_entries(pack: &str) -> Vec<PackEntry> {
499 let mut entries = Vec::new();
500 let mut current_name: Option<String> = None;
501 let mut current_header: String = String::new();
502 let mut current_body: String = String::new();
503
504 for line in pack.lines() {
505 if line.starts_with("## ") {
506 if let Some(name) = current_name.take() {
508 entries.push(PackEntry {
509 name,
510 header: std::mem::take(&mut current_header),
511 body: std::mem::take(&mut current_body),
512 });
513 }
514 let after = line.strip_prefix("## ").unwrap_or(line);
518 current_name = Some(after.split('/').next().unwrap_or("").trim().to_string());
519 current_header = line.to_string();
520 } else if current_name.is_some() {
521 if !current_body.is_empty() {
522 current_body.push('\n');
523 }
524 current_body.push_str(line);
525 }
526 }
527 if let Some(name) = current_name {
529 entries.push(PackEntry {
530 name,
531 header: current_header,
532 body: current_body,
533 });
534 }
535 entries
536}
537
538fn tokenize(text: &str) -> Vec<String> {
542 reliary_search::tokenize(text)
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum SliceDecision {
560 Slice,
562 Skip,
564}
565
566pub fn should_slice_for_query(query: &str) -> SliceDecision {
570 let text = query.to_ascii_lowercase();
571
572 if text.contains("bug") || text.contains("issue") || text.contains("wrong")
574 || text.contains("incorrect") || text.contains("missing") {
575 return SliceDecision::Slice;
576 }
577 if text.contains("call") || text.contains("caller") || text.contains("who uses")
578 || text.contains("references") || text.contains("called by") {
579 return SliceDecision::Slice;
580 }
581
582 if text.contains("value") || text.contains("threshold") || text.contains("parameter")
584 || text.contains("default") || text.contains("exactly") || text.contains("what number") {
585 return SliceDecision::Slice;
586 }
587
588 let func_indicators = [
592 "find_clusters", "find_clusters_global", "skelhash", "skeleton_hash",
593 "aggressive_skeleton_hash", "maxwell", "maxwellgate", "should_drop",
594 "detect_strategy", "detect_tabular", "detect_json", "callgraph",
595 "is_definition_line", "reliary_sift", "reliary-output",
596 ];
597 for indicator in &func_indicators {
598 if text.contains(indicator) {
599 return SliceDecision::Slice;
600 }
601 }
602
603 SliceDecision::Skip
606}
607
608pub fn adaptive_slice(pack: &str, query: &str, top_k: usize) -> (String, SliceDecision) {
611 let decision = should_slice_for_query(query);
612 match decision {
613 SliceDecision::Slice => (slice_pack_for_query(pack, query, top_k), decision),
614 SliceDecision::Skip => (String::new(), decision),
615 }
616}
617
618pub fn slice_pack_for_query(pack: &str, query: &str, top_k: usize) -> String {
622 let entries = parse_pack_entries(pack);
623 if entries.is_empty() {
624 return String::new();
625 }
626
627 let query_tokens = tokenize(query);
628 if query_tokens.is_empty() {
629 return entries
631 .into_iter()
632 .take(top_k)
633 .map(|e| format!("{}\n{}", e.header, e.body))
634 .collect::<Vec<_>>()
635 .join("\n\n");
636 }
637
638 let n_docs = entries.len() as f64;
640 let mut doc_tokens: Vec<Vec<String>> = Vec::with_capacity(entries.len());
641 for entry in &entries {
642 let combined = format!("{} {}", entry.name, entry.body);
644 doc_tokens.push(tokenize(&combined));
645 }
646
647 let mut df: FxHashMap<String, usize> = FxHashMap::default();
649 for tokens in &doc_tokens {
650 let unique: HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
651 for t in &query_tokens {
652 if unique.contains(t.as_str()) {
653 *df.entry(t.clone()).or_insert(0) += 1;
654 }
655 }
656 }
657
658 let total_len: usize = doc_tokens.iter().map(|d| d.len()).sum();
660 let avgdl = if n_docs > 0.0 {
661 total_len as f64 / n_docs
662 } else {
663 1.0
664 };
665
666 let mut scored: Vec<(f64, &PackEntry)> = Vec::with_capacity(entries.len());
668 for (i, entry) in entries.iter().enumerate() {
669 let doc_len = doc_tokens[i].len() as f64;
670 let mut score = 0.0;
671 let mut tf: FxHashMap<&str, usize> = FxHashMap::default();
673 for t in &doc_tokens[i] {
674 *tf.entry(t.as_str()).or_insert(0) += 1;
675 }
676 for qt in &query_tokens {
677 let d_freq = *df.get(qt).unwrap_or(&0) as f64;
678 if d_freq == 0.0 {
679 continue;
680 }
681 let idf = ((n_docs - d_freq + 0.5) / (d_freq + 0.5) + 1.0).ln();
682 let term_freq = *tf.get(qt.as_str()).unwrap_or(&0) as f64;
683 if term_freq > 0.0 {
684 let bm25 = idf * (term_freq * (1.2 + 1.0))
685 / (term_freq + 1.2 * (1.0 - 0.75 + 0.75 * (doc_len / avgdl)));
686 score += bm25;
687 }
688 }
689 let name_lower = entry.name.to_ascii_lowercase();
691 for qt in &query_tokens {
692 if name_lower == *qt {
693 score += 5.0; } else if name_lower.contains(qt.as_str()) {
695 score += 2.0; }
697 }
698 scored.push((score, entry));
699 }
700
701 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
703
704 let selected: Vec<&PackEntry> = scored
706 .into_iter()
707 .filter(|(s, _)| *s > 0.0)
708 .take(top_k)
709 .map(|(_, e)| e)
710 .collect();
711
712 if selected.is_empty() {
713 return String::new();
714 }
715
716 let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
721 let mut entries_by_name: FxHashMap<&str, &PackEntry> = FxHashMap::default();
723 for e in &entries {
724 entries_by_name.entry(e.name.as_str()).or_insert(e);
725 }
726 let mut expanded: Vec<&PackEntry> = selected.clone();
727 let mut expanded_names: HashSet<String> = selected_names.clone();
728 let mut expansion_count = 0;
729 let max_expansion = 15usize.saturating_sub(top_k.min(15));
730 for entry in &selected {
731 for line in entry.body.lines() {
732 let refs_text = if let Some(rest) = line.strip_prefix("Cross-refs:") {
733 Some(rest.trim())
734 } else if line.starts_with("L4:") && line.contains("caller") {
735 if let Some(pos) = line.find(": ") {
737 let after = &line[pos + 2..];
738 if let Some(last_colon) = after.rfind(": ") {
740 Some(&after[last_colon + 2..])
741 } else {
742 Some(after)
743 }
744 } else {
745 None
746 }
747 } else {
748 None
749 };
750 if let Some(refs) = refs_text {
751 for ref_name in refs.split(',').take(3) {
752 let ref_name = ref_name.trim();
753 if ref_name.is_empty() { continue; }
754 let ref_name_slash = format!("{}/", ref_name);
755 if let Some(e) = entries_by_name.get(ref_name).or_else(|| {
757 entries_by_name.values().find(|e| e.name.starts_with(&ref_name_slash))
758 }) {
759 if !selected_names.contains(&e.name) && !expanded_names.contains(&e.name) {
760 expanded_names.insert(e.name.clone());
761 expanded.push(e);
762 expansion_count += 1;
763 if expansion_count >= max_expansion { break; }
764 }
765 }
766 if expansion_count >= max_expansion { break; }
767 }
768 break; }
770 }
771 if expansion_count >= max_expansion { break; }
772 }
773
774 let total_entries = expanded.len();
775
776 let mut result = format!(
777 "# Sliced Pack — {} entries for query: \"{}\"\n\n",
778 total_entries,
779 query.chars().take(80).collect::<String>()
780 );
781 for entry in &expanded {
782 result.push_str(&format!("{}\n{}\n\n", entry.header, entry.body));
783 }
784 result
785}
786
787pub fn generate_hierarchical_pack(
789 path: &str,
790 format: PackFormat,
791 max_symbols_per_module: usize,
792) -> Result<HierarchicalPack, String> {
793 let db_path = format!(
794 "{}/.reliary/index.sqlite",
795 path.trim_end_matches('/')
796 );
797 let db = Connection::open(&db_path)
798 .map_err(|e| format!("cannot open index: {}", e))?;
799 reliary_search::schema::open_existing_db_safe(&db)
800 .map_err(|e| format!("index not initialized: {}", e))?;
801
802 let symbols = extract_symbols_from_index(&db)?;
803 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
804 let cross_refs = build_cross_refs_from_index(&db, &symbols);
805
806 let modules = detect_modules(&symbols);
808
809 let mut top_parts = vec![format!(
811 "# Holographic Pack — {} (top-level map)\n",
812 Path::new(path)
813 .file_name()
814 .map(|n| n.to_string_lossy().to_string())
815 .unwrap_or_else(|| path.to_string()),
816 )];
817
818 let mut module_packs: Vec<(String, String)> = Vec::new();
819
820 for (module_name, module_syms) in &modules {
821 let sym_count = module_syms.len();
822 let key_syms: Vec<String> = module_syms
823 .iter()
824 .filter(|s| !s.doc_comment.is_empty())
825 .take(5)
826 .map(|s| s.name.clone())
827 .collect();
828
829 top_parts.push(format!("## {}", module_name));
830 top_parts.push(format!("L0: {} symbols across this module", sym_count));
831 if !key_syms.is_empty() {
832 top_parts.push(format!("Key symbols: {}", key_syms.join(", ")));
833 }
834 top_parts.push(String::new());
835
836 let limited: Vec<&Symbol> = module_syms.iter().take(max_symbols_per_module).collect();
838 if limited.is_empty() {
839 continue;
840 }
841 let mut pack_parts = vec![format!("# Holographic Pack — {}\n", module_name)];
842 for sym in limited {
843 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
844 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
845 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
846 pack_parts.push(entry);
847 }
848 module_packs.push((module_name.clone(), pack_parts.join("\n")));
849 }
850
851 Ok(HierarchicalPack {
852 top_level: top_parts.join("\n"),
853 module_packs,
854 })
855}
856
857pub struct HierarchicalPack {
858 pub top_level: String,
859 pub module_packs: Vec<(String, String)>,
860}
861
862#[derive(Debug, Clone)]
867pub struct Symbol {
868 pub name: String,
869 pub signature: String,
870 pub file: String,
871 pub line: i32,
872 pub doc_comment: String,
873 pub block_id: i64,
874 pub end_line: i32,
875 pub indent: i32,
876}
877
878fn extract_symbols_from_index(db: &Connection) -> Result<Vec<Symbol>, String> {
883 let mut stmt = db
884 .prepare(
885 "SELECT o.phrase_id, o.file_id, o.line, o.block_id,
886 p.phrase, f.file_path,
887 b.end_line, b.indent
888 FROM occurrence o
889 JOIN phrases p ON o.phrase_id = p.id
890 JOIN file_map f ON o.file_id = f.id
891 JOIN block b ON o.block_id = b.block_id
892 WHERE o.is_def = 1
893 ORDER BY f.file_path, o.line",
894 )
895 .map_err(|e| format!("query symbols: {}", e))?;
896
897 let rows = stmt
898 .query_map([], |row| {
899 let phrase: String = row.get(4)?;
900 let file_path: String = row.get(5)?;
901 let line: i32 = row.get(2)?;
902 let block_id: i64 = row.get(3)?;
903 let end_line: i32 = row.get(6)?;
904 let indent: i32 = row.get(7)?;
905 Ok((phrase, file_path, line, block_id, end_line, indent))
906 })
907 .map_err(|e| format!("query symbols: {}", e))?;
908
909 let mut symbols: Vec<Symbol> = Vec::new();
910 let mut seen: HashSet<(String, String)> = HashSet::new();
914
915 let mut lines_cache: FxHashMap<String, Vec<String>> = FxHashMap::default();
918
919 for row in rows {
920 let (_phrase, file_path, line, block_id, end_line, indent) =
921 row.map_err(|e| format!("row: {}", e))?;
922
923 if !is_source_file(&file_path) {
925 continue;
926 }
927
928 if !lines_cache.contains_key(&file_path) {
930 let lines_vec: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&file_path) {
931 meta.lines.clone()
932 } else {
933 std::fs::read_to_string(&file_path)
934 .map(|c| c.lines().map(String::from).collect())
935 .unwrap_or_default()
936 };
937 lines_cache.insert(file_path.clone(), lines_vec);
938 }
939
940 let lines = &lines_cache[&file_path];
941
942 let signature = read_signature_line(lines, line);
944
945 let ends_with_brace = signature.trim_end().ends_with('{');
952 let classified =
953 reliary_search::structural::classify_structural(&signature, 0, ends_with_brace, false);
954 let real_name = if classified.is_def {
955 classified.defined_name.map(|s| s.to_string()).unwrap_or_default()
956 } else if is_definition_like(&signature) {
957 extract_name_from_signature(&signature)
961 } else {
962 String::new()
963 };
964 if real_name.len() < 3 {
965 continue;
966 }
967
968 if is_common_word(&real_name) {
970 continue;
971 }
972
973 if real_name.starts_with("__") && real_name.ends_with("__") {
975 continue;
976 }
977
978 let key = (real_name.clone(), file_path.clone());
980 if seen.contains(&key) {
981 continue;
982 }
983 seen.insert(key);
984
985 let doc_comment = read_doc_comment(lines, line);
987
988 symbols.push(Symbol {
989 name: real_name,
990 signature,
991 file: file_path,
992 line,
993 doc_comment,
994 block_id,
995 end_line,
996 indent,
997 });
998 }
999
1000 Ok(symbols)
1001}
1002
1003fn extract_name_from_signature(sig: &str) -> String {
1008 let cleaned = sig.trim();
1009
1010 let mut rest = cleaned;
1012 let keywords = [
1013 "pub(crate) ", "pub(super) ", "pub ", "export ", "async ", "extern ",
1014 "static ", "inline ", "virtual ", "override ", "final ", "abstract ",
1015 "unsafe ", "mut ",
1016 ];
1017 loop {
1018 let mut changed = false;
1019 for kw in &keywords {
1020 if rest.starts_with(kw) {
1021 rest = &rest[kw.len()..];
1022 changed = true;
1023 break;
1024 }
1025 }
1026 if !changed {
1027 break;
1028 }
1029 }
1030
1031 let decl_kws = [
1033 "fn ", "function ", "def ", "func ", "struct ", "class ", "enum ",
1034 "trait ", "interface ", "type ", "impl ", "const ", "let ", "var ",
1035 "val ",
1036 ];
1037 for kw in &decl_kws {
1038 if rest.starts_with(kw) {
1039 rest = &rest[kw.len()..];
1040 break;
1041 }
1042 }
1043
1044 if rest.starts_with("impl ") {
1047 rest = &rest[5..];
1048 }
1049
1050 let end = rest
1052 .find(['(', '<', ':', '{', ' ', '=', ';'])
1053 .unwrap_or(rest.len());
1054 let name = rest[..end].trim();
1055
1056 let name = name.trim_start_matches('\'');
1058
1059 if name.is_empty() || name.len() < 2 {
1060 return String::new();
1061 }
1062
1063 if !name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
1065 return String::new();
1066 }
1067 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1068 return String::new();
1069 }
1070
1071 name.to_string()
1072}
1073
1074fn is_source_file(path: &str) -> bool {
1076 if path.contains("/bench/")
1079 || path.contains("/tests/")
1080 || path.contains("/test/")
1081 || path.contains("/target/")
1082 || path.contains("/vendor/")
1083 || path.contains("/node_modules/")
1084 || path.contains("/__pycache__/")
1085 || path.contains("/.git/")
1086 {
1087 return false;
1088 }
1089 let ext = Path::new(path)
1090 .extension()
1091 .and_then(|e| e.to_str())
1092 .unwrap_or("");
1093 matches!(
1099 ext,
1100 "rs" | "py" | "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs"
1101 | "go" | "java" | "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hh"
1102 | "rb" | "swift" | "kt" | "kts" | "scala" | "clj" | "ex" | "exs"
1103 | "elm" | "fs" | "fsx" | "ml" | "mli" | "hs" | "lua" | "php" | "pl"
1104 | "cs" | "dart" | "groovy" | "m" | "mm" | "jl" | "r" | "v"
1105 )
1106}
1107
1108fn is_definition_like(line: &str) -> bool {
1112 let stripped = line.trim();
1113 if stripped.is_empty() {
1114 return false;
1115 }
1116
1117 if stripped.starts_with("//")
1119 || stripped.starts_with('#')
1120 || stripped.starts_with("/*")
1121 || stripped.starts_with("///")
1122 || stripped.starts_with("--")
1123 || stripped.starts_with('\'')
1124 {
1125 return false;
1126 }
1127
1128 if stripped.starts_with("if ")
1130 || stripped.starts_with("for ")
1131 || stripped.starts_with("while ")
1132 || stripped.starts_with("return ")
1133 || stripped.starts_with("match ")
1134 || stripped.starts_with("switch ")
1135 || stripped.starts_with("let ")
1136 || stripped.starts_with("let mut")
1137 || stripped.starts_with("const ")
1138 || stripped.starts_with("var ")
1139 || stripped.starts_with("static ") || stripped.starts_with("type ") || stripped.starts_with("enum ") || stripped.starts_with("f.write")
1143 || stripped.starts_with("println")
1144 || stripped.starts_with("print")
1145 || stripped.starts_with("eprintln")
1146 {
1147 return false;
1148 }
1149
1150 let check = stripped.trim_end_matches([';', ',']);
1151
1152 let ends_with_brace = check.ends_with('{')
1164 || (check.ends_with('}')
1165 && check.matches('{').count() == 1
1166 && check.matches('}').count() == 1);
1167 if check.contains('(') && ends_with_brace {
1168 let before_paren = check.split('(').next().unwrap_or("").trim();
1169 let mut name_part = before_paren;
1173 for kw in &["pub(crate) ", "pub(super) ", "pub ", "async ", "unsafe ",
1174 "extern ", "fn ", "function ", "func ", "export ",
1175 "static ", "inline ", "const ", "final ", "override "] {
1176 if name_part.starts_with(kw) {
1177 name_part = &name_part[kw.len()..];
1178 }
1179 }
1180 if !name_part.contains("::")
1183 && !name_part.contains('.')
1184 && !name_part.contains("->")
1185 && !name_part.contains("||")
1186 && !name_part.contains('=')
1187 && !name_part.contains(' ')
1188 && name_part.len() > 3
1189 {
1190 return true;
1191 }
1192 }
1193
1194 if check.starts_with("def ") && check.ends_with(':') {
1196 return true;
1197 }
1198
1199 if check.ends_with('{') {
1201 let lower = check.to_ascii_lowercase();
1202 if lower.contains(" struct ")
1203 || lower.contains(" class ")
1204 || lower.contains(" enum ")
1205 || lower.contains(" trait ")
1206 || lower.contains(" interface ")
1207 || lower.contains(" impl ")
1208 || lower.starts_with("struct ")
1209 || lower.starts_with("class ")
1210 || lower.starts_with("enum ")
1211 || lower.starts_with("trait ")
1212 || lower.starts_with("interface ")
1213 || lower.starts_with("impl ")
1214 || lower.starts_with("pub struct ")
1215 || lower.starts_with("pub enum ")
1216 || lower.starts_with("pub trait ")
1217 {
1218 return true;
1219 }
1220 }
1221
1222 if check.ends_with('{') && (check.starts_with("export interface ") || check.starts_with("interface ") || check.starts_with("export type ") || check.starts_with("type ")) {
1224 return true;
1225 }
1226
1227 false
1228}
1229
1230fn is_common_word(word: &str) -> bool {
1232 if word.len() < 3 {
1234 return true;
1235 }
1236 const COMMON: &[&str] = &[
1238 "the", "and", "for", "not", "are", "but", "you", "all", "can", "her",
1239 "was", "one", "our", "out", "has", "his", "how", "its", "may", "new",
1240 "now", "old", "see", "way", "who", "did", "get", "let", "say", "she",
1241 "too", "use", "any", "ask", "bad", "big", "day", "end", "few", "got",
1242 "had", "him", "his", "how", "its", "man", "men", "put", "run", "set",
1243 "try", "two", "use", "via", "yet", "also", "code", "data", "file",
1244 "from", "have", "into", "just", "like", "make", "more", "must",
1245 "only", "over", "some", "such", "than", "them", "then", "they",
1246 "this", "that", "when", "what", "where", "which", "while", "will",
1247 "with", "work", "would", "your", "about", "after", "again", "before",
1248 "being", "below", "could", "every", "first", "found", "great",
1249 "group", "having", "here", "high", "into", "itself", "just",
1250 "large", "last", "left", "like", "long", "made", "many", "most",
1251 "much", "never", "next", "once", "other", "part", "place", "right",
1252 "same", "should", "show", "since", "small", "some", "still",
1253 "such", "take", "their", "them", "then", "there", "these",
1254 "they", "thing", "think", "this", "those", "through", "time",
1255 "under", "until", "very", "well", "were", "what", "when",
1256 "where", "which", "while", "will", "with", "without", "would",
1257 "write", "your", "index", "free", "grammar", "instead", "tool",
1258 "tree", "language", "read", "work", "reliary",
1259 ];
1260 static COMMON_SET: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
1262 let set = COMMON_SET.get_or_init(|| std::collections::HashSet::from_iter(COMMON.iter().copied()));
1263 let lower = word.to_ascii_lowercase();
1264 set.contains(lower.as_str())
1265}
1266
1267fn read_signature_line(lines: &[String], line: i32) -> String {
1268 lines
1269 .get(line.max(0) as usize)
1270 .map(|s| s.trim().to_string())
1271 .unwrap_or_default()
1272}
1273
1274fn read_doc_comment(lines: &[String], def_line: i32) -> String {
1275 let def_idx = (def_line.max(0) as usize).min(lines.len().saturating_sub(1));
1276
1277 let mut doc_parts: Vec<String> = Vec::new();
1279 let mut j = def_idx.saturating_sub(1);
1280
1281 if j < lines.len() && lines[j].trim().ends_with("*/") {
1283 let mut block: Vec<String> = Vec::new();
1284 while j > 0 {
1285 let l = lines[j].trim();
1286 block.insert(0, l.to_string());
1287 if l.starts_with("/**") || l.starts_with("/*") {
1288 break;
1289 }
1290 j -= 1;
1291 }
1292 for bl in &block {
1293 let clean = bl
1294 .trim()
1295 .trim_start_matches("/**")
1296 .trim_start_matches("/*")
1297 .trim_end_matches("*/")
1298 .trim()
1299 .trim_start_matches('*')
1300 .trim();
1301 if !clean.is_empty() && !clean.starts_with('@') && clean != "/**" {
1302 doc_parts.insert(0, clean.to_string());
1303 }
1304 }
1305 }
1306
1307 if doc_parts.is_empty() {
1308 j = def_idx.saturating_sub(1);
1310 while j > 0 {
1311 let l = lines[j].trim();
1312 if let Some(rest) = l.strip_prefix("///") {
1313 doc_parts.insert(0, rest.trim().to_string());
1314 } else if let Some(rest) = l.strip_prefix("//") {
1315 doc_parts.insert(0, rest.trim().to_string());
1316 } else if l.starts_with('#') && !l.starts_with("#[") {
1317 doc_parts.insert(0, l[1..].trim().to_string());
1318 } else if l.is_empty() {
1319 } else {
1321 break;
1322 }
1323 j -= 1;
1324 }
1325 }
1326
1327 doc_parts.join(" ")
1328}
1329
1330fn is_noise_line(line: &str) -> bool {
1342 let trimmed = line.trim();
1343 if trimmed.is_empty() {
1344 return true;
1345 }
1346 if trimmed.len() < 15 {
1349 return true;
1350 }
1351 if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1353 return true;
1354 }
1355 if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1358 return true;
1359 }
1360 if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1362 return true;
1363 }
1364 if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1366 return true;
1367 }
1368 if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1370 return true;
1371 }
1372 if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1374 return true;
1375 }
1376 if trimmed.starts_with("assert") {
1378 return true;
1379 }
1380 if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1382 return true;
1383 }
1384 if (trimmed.starts_with("let ") || trimmed.starts_with("let mut "))
1388 && !trimmed.contains("(")
1389 && !trimmed.contains("{")
1390 && trimmed.ends_with(";")
1391 {
1392 return true;
1393 }
1394 let trimmed_for_return = if let Some(comment_start) = trimmed.find("//") {
1398 trimmed[..comment_start].trim()
1399 } else {
1400 trimmed
1401 };
1402 if let Some(rest) = trimmed_for_return.strip_prefix("return ") {
1403 if let Some(inner) = rest.strip_suffix(";") {
1406 let inner = inner.trim();
1407 if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1408 return true;
1410 }
1411 }
1412 if rest.contains(".lower()") || rest.contains(".upper()") {
1414 return true;
1415 }
1416 }
1417 if trimmed.starts_with("if isinstance(") {
1419 return true;
1420 }
1421 if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1423 return true;
1424 }
1425 if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1427 return true;
1428 }
1429 if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1431 return true;
1432 }
1433 if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1435 return true;
1436 }
1437 if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1439 return true;
1440 }
1441 if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1444 return true;
1445 }
1446 if let Some(rest) = trimmed.strip_prefix("self.") {
1448 if rest.starts_with("assert") || rest.contains(".assert") || rest.starts_with("setUp")
1449 || rest.starts_with("tearDown") || rest.starts_with("set_up")
1450 || rest.starts_with("tear_down") || rest.starts_with("skip(")
1451 || rest.starts_with("fixture") || rest.starts_with("patch(")
1452 {
1453 return true;
1454 }
1455 }
1456 if trimmed.contains(".append(") || trimmed.contains(".push(")
1459 || trimmed.contains(".insert(") || trimmed.contains(".remove(")
1460 || trimmed.contains(".clear(") || trimmed.contains(".drop(")
1461 || trimmed.contains(".len()") || trimmed.contains(".is_empty()")
1462 || trimmed.contains(".expect(") || trimmed.contains(".collect()")
1463 || trimmed.contains(".clone(") || trimmed.contains(".iter()")
1464 {
1465 return true;
1466 }
1467 if trimmed == "break;" || trimmed == "continue;"
1469 || trimmed == "break" || trimmed == "continue" {
1470 return true;
1471 }
1472 if trimmed.starts_with("from ") {
1474 return true;
1475 }
1476 if let Some(stripped_return) = trimmed.strip_prefix("return ") {
1478 let rest = stripped_return.trim().trim_end_matches(';').trim();
1479 if matches!(rest, "true" | "false" | "None" | "0" | "1" | "Self" | "-1") {
1480 return true;
1481 }
1482 }
1483 if trimmed.starts_with("log::") || trimmed.starts_with("tracing::")
1485 || trimmed.starts_with("debug!") || trimmed.starts_with("info!")
1486 || trimmed.starts_with("warn!") || trimmed.starts_with("error!")
1487 || trimmed.starts_with("print!") || trimmed.starts_with("eprint!")
1488 || trimmed.starts_with("println!") || trimmed.starts_with("eprintln!")
1489 || trimmed.starts_with("dbg!") || trimmed.starts_with("todo!")
1490 {
1491 return true;
1492 }
1493 if trimmed.starts_with("let _ =") || trimmed.starts_with("let _mut _ =")
1495 || trimmed.starts_with("let (_, _) =") || trimmed.starts_with("let (_, _, _) =")
1496 {
1497 return true;
1498 }
1499 let code_only = if let Some(comment_start) = trimmed.find("//") {
1502 trimmed[..comment_start].trim()
1503 } else {
1504 trimmed
1505 };
1506 if code_only.ends_with(".ok()?") || code_only.ends_with(".ok();")
1507 || code_only.contains(".expect(\"") || code_only.contains(".unwrap_or(")
1508 || code_only.contains(".unwrap_or_default(") || code_only.contains(".unwrap_or_else(")
1509 || (code_only.starts_with("return ") && code_only.contains(".ok()"))
1510 || (code_only.starts_with("return ") && code_only.contains(".err()"))
1511 || (code_only.starts_with("return ") && code_only.contains(".unwrap()"))
1512 {
1513 return true;
1514 }
1515 if let Some(eq_pos) = trimmed.find('=') {
1516 if eq_pos > 0 && !trimmed[eq_pos+1..].trim().starts_with("\"")
1517 && trimmed.contains(".to_string(")
1518 && !trimmed.contains("unsafe ") && !trimmed.contains("transmute")
1519 {
1520 return true;
1521 }
1522 }
1523 false
1524}
1525
1526
1527fn extract_surprise_from_body(
1533 body: &str,
1534 skeleton_freq: &FxHashMap<String, usize>,
1535) -> Vec<String> {
1536 let mut surprises: Vec<String> = Vec::new();
1537 let mut seen: HashSet<String> = HashSet::new();
1538
1539 let lines: Vec<&str> = body.lines().collect();
1540
1541 if let Some(m) = regex_capture(r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{", body) {
1548 add_surprise(&mut surprises, &mut seen,
1549 format!("auto-pass for input below {} chars (no gates applied)", m));
1550 }
1551
1552 if body.contains("expect_dash") || body.contains("j == 8") && body.contains("j == 13") && body.contains("j == 23") {
1555 add_surprise(&mut surprises, &mut seen,
1556 "UUID dash positions: 8, 13, 18, 23 (standard 8-4-4-4-12)".to_string());
1557 }
1558
1559 if body.contains("he - i") || body.contains("hex_len") {
1562 if let Some(m) = regex_capture(r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)", body) {
1563 add_surprise(&mut surprises, &mut seen,
1564 format!("hex-hash length range: {} to {}", m.split(',').next().unwrap_or(""), m.split(',').nth(1).unwrap_or("")));
1565 }
1566 }
1567
1568 if body.contains("5381") && body.contains("wrapping_mul") && body.contains("33") {
1571 add_surprise(&mut surprises, &mut seen,
1572 "djb3-33 hash: starts at 5381, multiplies by 33 (Bernstein variant)".to_string());
1573 }
1574
1575 if body.contains("bytes[") && body.contains("while") && !body.contains("chars()") {
1578 add_surprise(&mut surprises, &mut seen,
1579 "byte-indexed processing (not char-indexed)".to_string());
1580 }
1581
1582 if let Some(m) = regex_capture(r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)", body) {
1585 add_surprise(&mut surprises, &mut seen,
1586 format!("sentinel return: {} for empty input", m));
1587 }
1588
1589if body.contains("import") && body.contains("count") && body.contains('[') {
1592 add_surprise(&mut surprises, &mut seen,
1593 "imports collapsed into a single label '[N imports]'".to_string());
1594 }
1595
1596 if body.contains("flush") || (body.contains("if ") && body.contains("emit") && body.contains("reset")) {
1599 add_surprise(&mut surprises, &mut seen,
1600 "lazy flush: label emitted on next non-matching line, not immediately".to_string());
1601 }
1602
1603 if body.contains("Error") && body.contains("Comment") {
1606 let error_pos = body.find("Error").unwrap_or(0);
1607 let comment_pos = body.find("Comment").unwrap_or(0);
1608 if error_pos < comment_pos && error_pos > 0 && comment_pos > 0 {
1609 add_surprise(&mut surprises, &mut seen,
1610 "Error check runs BEFORE Comment check (priority order)".to_string());
1611 } else if comment_pos < error_pos && comment_pos > 0 && error_pos > 0 {
1612 add_surprise(&mut surprises, &mut seen,
1613 "Comment check runs BEFORE Error check (potential bug: errors in comments are missed)".to_string());
1614 }
1615 }
1616
1617 if regex_capture(r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+", body).is_some() {
1620 if body.contains("&&") {
1621 add_surprise(&mut surprises, &mut seen,
1622 "gates are AND-combined: ALL must fail for None return".to_string());
1623 } else if body.contains("||") {
1624 add_surprise(&mut surprises, &mut seen,
1625 "gates are OR-combined: ANY failing gate returns None".to_string());
1626 }
1627 }
1628
1629 if body.contains("aggressive") || body.contains("collapse") {
1632 if let Some(m) = regex_capture(r"\w+\s*>\s*\w+\s*\+\s*(\d+)", body) {
1633 add_surprise(&mut surprises, &mut seen,
1634 format!("minimum word length threshold: {} (below this, words kept verbatim)", m));
1635 }
1636 }
1637
1638 if let Some(m) = regex_capture(r"let\s+(\w+)\s*=\s*skeleton\(\w+\)", body) {
1641 add_surprise(&mut surprises, &mut seen,
1642 format!("operates on the SKELETON of input, not the original text (via {})", m));
1643 }
1644
1645 if body.contains("replace_all") && body.contains("&") {
1648 let placeholders: Vec<&str> = body
1649 .split(".replace_all")
1650 .skip(1)
1651 .filter_map(|s| {
1652 if let Some(idx) = s.find('"') {
1653 let after = &s[idx..];
1654 if let Some(end) = after[1..].find('"') {
1655 return Some(&after[1..=end]);
1656 }
1657 }
1658 None
1659 })
1660 .collect();
1661 if !placeholders.is_empty() {
1662 add_surprise(&mut surprises, &mut seen,
1663 format!("regex-based replacement: {}", placeholders.iter().take(3).cloned().collect::<Vec<_>>().join(", ")));
1664 }
1665 }
1666
1667if body.contains("num_end") && body.contains('.') {
1670 add_surprise(&mut surprises, &mut seen,
1671 "version detection: requires exactly three dot-separated numeric components (X.Y.Z)".to_string());
1672 }
1673
1674 let mut seen_nums: HashSet<String> = HashSet::new();
1676 let mut seen_conds: HashSet<String> = HashSet::new();
1677
1678 for (line_idx, line) in lines.iter().enumerate() {
1679 let trimmed = line.trim();
1680 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#")
1681 || trimmed.starts_with("///") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
1682 continue;
1683 }
1684 let code = trimmed;
1685
1686 for num in find_magic_numbers(code) {
1688 let key = format!("{}_{}", num, line_idx);
1689 if !seen_nums.contains(&key) {
1690 seen_nums.insert(key);
1691 let labeled = label_number(&num, code);
1692 if !labeled.starts_with("constant: 0") && !labeled.starts_with("constant: 1") {
1693 add_surprise(&mut surprises, &mut seen, labeled);
1694 }
1695 }
1696 }
1697
1698 if code.contains("if ") && (code.contains("&&") || code.contains("||")) {
1700 if let Some(cond) = extract_condition(code) {
1701 let cond_key: String = cond.chars().take(40).collect();
1702 if cond.len() > 10 && cond.len() < 200 && !seen_conds.contains(&cond_key) {
1703 seen_conds.insert(cond_key);
1704 add_surprise(&mut surprises, &mut seen,
1705 format!("compound condition: {}", cond));
1706 }
1707 }
1708 }
1709
1710 if line_idx < 15 && code.contains("if ") && code.contains("return") {
1712 if let Some(ret) = extract_return_value(code) {
1713 if !ret.is_empty() && ret != "true" && ret != "false" {
1714 add_surprise(&mut surprises, &mut seen,
1715 format!("early return → {}", ret));
1716 }
1717 }
1718 }
1719
1720 if code.contains(".wrapping_") {
1722 let ops: Vec<&str> = code
1723 .split(".wrapping_")
1724 .skip(1)
1725 .filter_map(|s| s.split('(').next())
1726 .collect();
1727 if !ops.is_empty() {
1728 add_surprise(&mut surprises, &mut seen,
1729 format!("wrapping: {}", ops.join(", ")));
1730 }
1731 }
1732 if code.contains("unwrap_or(") {
1733 if let Some(def) = extract_unwrap_default(code) {
1734 if !def.is_empty() {
1735 add_surprise(&mut surprises, &mut seen,
1736 format!("fallback: unwrap_or({})", def));
1737 }
1738 }
1739 }
1740
1741 if code.contains("with_capacity(") {
1743 if let Some(n) = extract_capacity(code) {
1744 add_surprise(&mut surprises, &mut seen,
1745 format!("capacity: {}", n));
1746 }
1747 }
1748
1749 if let Some(comment_start) = code.find("//") {
1751 let comment = code[comment_start + 2..].trim().to_ascii_lowercase();
1752 let comment_text = code[comment_start + 2..].trim();
1753 for sig in ["key", "difference", "note", "important", "surprise",
1754 "critical", "deliberate", "only fires", "not regex", "must"]
1755 {
1756 if comment.contains(sig) {
1757 add_surprise(&mut surprises, &mut seen,
1758 format!("source note: {}", comment_text));
1759 break;
1760 }
1761 }
1762 }
1763 }
1764
1765 for (line_idx, line) in lines.iter().enumerate() {
1767 if line_idx == 0 { continue; } let trimmed = line.trim();
1769 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
1770 continue;
1771 }
1772 if is_noise_line(trimmed) {
1773 continue;
1774 }
1775 let skel = aggressive_skeleton(trimmed);
1776 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 10 {
1777 continue;
1778 }
1779 let freq = skeleton_freq.get(&skel).copied().unwrap_or(0);
1780 if freq == 1 {
1781 let has_signal = trimmed.contains('(')
1786 || trimmed.contains("==") || trimmed.contains("!=")
1787 || trimmed.contains(" < ") || trimmed.contains(" > ")
1788 || trimmed.contains(">=") || trimmed.contains("<=")
1789 || trimmed.contains("&&") || trimmed.contains("||")
1790 || trimmed.contains("wrapping_") || trimmed.contains("unwrap_or")
1791 || trimmed.contains("match ") || trimmed.contains("=>");
1792 if has_signal {
1793 let snippet: String = trimmed.chars().take(80).collect();
1794 add_surprise(&mut surprises, &mut seen,
1795 format!("unique line: {}", snippet));
1796 }
1797 }
1798 }
1799
1800 let pre_dedup_len = surprises.len();
1803 let mut final_surprises: Vec<String> = Vec::new();
1804 let mut seen_fragments: HashSet<String> = HashSet::new();
1805 for s in surprises {
1806 let fragment: String = s
1808 .chars()
1809 .filter(|c| !c.is_whitespace())
1810 .take(40)
1811 .collect();
1812 if seen_fragments.contains(&fragment) {
1813 continue;
1814 }
1815 seen_fragments.insert(fragment);
1816 final_surprises.push(s);
1817 }
1818 let surprises = final_surprises;
1819 let post_dedup_len = surprises.len();
1820 if pre_dedup_len > post_dedup_len + 1 {
1821 }
1823 let pattern_count = surprises.iter()
1825 .filter(|s| !s.starts_with("rare pattern") && !s.starts_with("unique line"))
1826 .count();
1827 let freq_count = surprises.len() - pattern_count;
1828
1829 let limit = if pattern_count >= 3 {
1833 pattern_count.min(8) } else if pattern_count >= 1 {
1835 (pattern_count + freq_count.min(2)).min(8)
1836 } else {
1837 0
1839 };
1840
1841 surprises.into_iter().take(limit).collect()
1842}
1843
1844fn add_surprise(surprises: &mut Vec<String>, seen: &mut HashSet<String>, s: String) {
1845 let key: String = s.chars().take(60).collect();
1846 if !seen.contains(&key) {
1847 seen.insert(key);
1848 surprises.push(s);
1849 }
1850}
1851
1852fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1855 if pattern == r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{" {
1857 return manual_capture_len_guard(text);
1858 }
1859 if pattern == r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)" {
1860 return manual_capture_range(text);
1861 }
1862 if pattern == r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)" {
1863 return manual_capture_sentinel(text);
1864 }
1865 if pattern == r"\w+\s*>\s*\w+\s*\+\s*(\d+)" {
1866 return manual_capture_gt_plus(text);
1867 }
1868 if pattern == r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+" {
1869 return if text.contains("_threshold") { Some("found".to_string()) } else { None };
1870 }
1871 if pattern == r"let\s+(\w+)\s*=\s*skeleton\(\w+\)" {
1872 return manual_capture_skeleton_let(text);
1873 }
1874 None
1875}
1876
1877fn manual_capture_len_guard(text: &str) -> Option<String> {
1878 let bytes = text.as_bytes();
1879 let mut i = 0;
1880 while i < bytes.len() {
1881 if i + 2 < bytes.len() && bytes[i] == b'i' && bytes[i+1] == b'f' {
1882 i += 2;
1884 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1885 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1887 while i + 4 < bytes.len() && &bytes[i..i+5] == b".len(" { i += 5; while i < bytes.len() && bytes[i] != b')' { i += 1; } if i < bytes.len() { i += 1; } }
1889 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1891 if i < bytes.len() && bytes[i] == b'<' { i += 1; }
1892 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1893 let start = i;
1895 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1896 if i > start {
1897 return Some(String::from_utf8_lossy(&bytes[start..i]).to_string());
1898 }
1899 }
1900 i += 1;
1901 }
1902 None
1903}
1904
1905fn manual_capture_range(text: &str) -> Option<String> {
1906 let bytes = text.as_bytes();
1907 let mut i = 0;
1908 while i < bytes.len() {
1909 if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i+1] == b'=' {
1910 i += 2;
1911 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1912 let start = i;
1913 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1914 if i > start {
1915 let first = String::from_utf8_lossy(&bytes[start..i]).to_string();
1916 while i < bytes.len() && bytes[i] != b'<' { i += 1; }
1918 if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i+1] == b'=' { i += 2; }
1919 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1920 let start2 = i;
1921 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1922 if i > start2 {
1923 let second = String::from_utf8_lossy(&bytes[start2..i]).to_string();
1924 return Some(format!("{},{}", first, second));
1925 }
1926 }
1927 }
1928 i += 1;
1929 }
1930 None
1931}
1932
1933fn manual_capture_sentinel(text: &str) -> Option<String> {
1934 if let Some(idx) = text.find("is_empty") {
1935 let after = text.get(idx + 9..)?;
1938 if let Some(ret_idx) = after.find("return") {
1939 let after_ret = &after[ret_idx + 6..];
1940 let end = after_ret
1941 .find([';', '}', '\n'])
1942 .unwrap_or(after_ret.len());
1943 return Some(after_ret[..end].trim().to_string());
1944 }
1945 }
1946 None
1947}
1948
1949fn manual_capture_gt_plus(text: &str) -> Option<String> {
1950 if let Some(idx) = text.find('>') {
1951 let after = &text[idx + 1..];
1952 let trimmed = after.trim_start();
1954 let after_ident = trimmed
1956 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1957 let trimmed2 = after_ident.trim_start();
1958 if let Some(rest) = trimmed2.strip_prefix('+') {
1959 let trimmed3 = rest.trim_start();
1960 let end = trimmed3
1961 .find(|c: char| !c.is_ascii_digit())
1962 .unwrap_or(trimmed3.len());
1963 if end > 0 {
1964 return Some(trimmed3[..end].to_string());
1965 }
1966 }
1967 }
1968 None
1969}
1970
1971fn manual_capture_skeleton_let(text: &str) -> Option<String> {
1972 if let Some(idx) = text.find("let ") {
1973 let after = &text[idx + 4..];
1974 let after_ident = after
1975 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1976 let trimmed = after_ident.trim_start();
1977 if let Some(after_eq) = trimmed.strip_prefix('=') {
1978 let after_eq = after_eq.trim_start();
1979 if let Some(sk_idx) = after_eq.find("skeleton(") {
1980 let before = &after_eq[..sk_idx].trim();
1981 if !before.is_empty() {
1982 return Some(before.to_string());
1983 }
1984 }
1985 }
1986 }
1987 None
1988}
1989
1990fn find_magic_numbers(code: &str) -> Vec<String> {
1991 let mut nums = Vec::new();
1992 let bytes = code.as_bytes();
1994 let mut i = 0;
1995 while i < bytes.len() {
1996 if (bytes[i] == b'>' || bytes[i] == b'<' || bytes[i] == b'=' || bytes[i] == b'!')
1997 && i + 1 < bytes.len()
1998 {
1999 i += 1;
2001 if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
2002 i += 1;
2003 }
2004 while i < bytes.len() && bytes[i] == b' ' {
2006 i += 1;
2007 }
2008 let start = i;
2010 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
2011 i += 1;
2012 }
2013 if i > start {
2014 let num = String::from_utf8_lossy(&bytes[start..i]).to_string();
2015 if num != "0" && num != "1" {
2016 nums.push(num);
2017 }
2018 }
2019 } else {
2020 i += 1;
2021 }
2022 }
2023 nums
2024}
2025
2026fn label_number(num: &str, code: &str) -> String {
2027 let lower = code.to_ascii_lowercase();
2028 if lower.contains("uuid") || (num == "8" || num == "13" || num == "18" || num == "23") {
2029 format!("UUID position: {}", num)
2030 } else if lower.contains("hash") || lower.contains("hex") {
2031 format!("hex-hash bound: {}", num)
2032 } else if lower.contains("entropy") || lower.contains("threshold") {
2033 format!("threshold: {}", num)
2034 } else if lower.contains("len") || lower.contains("length") || lower.contains("count") {
2035 format!("length: {}", num)
2036 } else if lower.contains("capacity") {
2037 format!("capacity: {}", num)
2038 } else {
2039 format!("constant: {}", num)
2040 }
2041}
2042
2043fn extract_condition(code: &str) -> Option<String> {
2044 let if_pos = code.find("if ")?;
2045 let after = &code[if_pos + 3..];
2046 let end = after
2048 .find(['{', '\n'])
2049 .unwrap_or(after.len());
2050 Some(after[..end].trim().to_string())
2051}
2052
2053fn extract_return_value(code: &str) -> Option<String> {
2054 let ret_pos = code.find("return")?;
2055 let after = &code[ret_pos + 6..];
2056 let end = after.find([';', '}']).unwrap_or(after.len());
2057 Some(after[..end].trim().to_string())
2058}
2059
2060fn extract_unwrap_default(code: &str) -> Option<String> {
2061 let pos = code.find("unwrap_or(")?;
2062 let after = &code[pos + 10..];
2063 let end = after.find(')')?;
2064 Some(after[..end].trim().to_string())
2065}
2066
2067fn extract_capacity(code: &str) -> Option<String> {
2068 let pos = code.find("with_capacity(")?;
2069 let after = &code[pos + 14..];
2070 let end = after.find(')')?;
2071 Some(after[..end].trim().to_string())
2072}
2073
2074fn build_cross_refs_from_index(
2084 db: &Connection,
2085 symbols: &[Symbol],
2086) -> FxHashMap<String, Vec<String>> {
2087 build_cross_refs_inner(db, symbols).unwrap_or_default()
2089}
2090
2091fn build_cross_refs_inner(
2092 db: &Connection,
2093 symbols: &[Symbol],
2094) -> Result<FxHashMap<String, Vec<String>>, rusqlite::Error> {
2095 let def_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2096 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
2097 let mut ref_set: FxHashMap<String, HashSet<String>> = FxHashMap::default();
2098
2099 let mut all_pairs_stmt = db.prepare(
2104 "SELECT o.file_id, p.phrase, o.is_def
2105 FROM occurrence o
2106 JOIN phrases p ON o.phrase_id = p.id
2107 WHERE length(p.phrase) BETWEEN 3 AND 30
2108 ORDER BY o.file_id
2109 LIMIT 200000",
2110 )?;
2111
2112 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
2114 for row in all_pairs_stmt
2115 .query_map([], |row| {
2116 Ok((
2117 row.get::<_, i64>(0)?,
2118 row.get::<_, String>(1)?,
2119 row.get::<_, i64>(2)? != 0,
2120 ))
2121 })?
2122 .flatten()
2123 {
2124 let (file_id, phrase, is_def) = row;
2125 file_to_phrases
2126 .entry(file_id)
2127 .or_default()
2128 .push((phrase, is_def));
2129 }
2130
2131 let mut id_to_path: FxHashMap<i64, String> = FxHashMap::default();
2136 {
2137 let mut stmt = db.prepare("SELECT id, file_path FROM file_map ORDER BY file_path")?;
2138 let mut rows = stmt.query([])?;
2139 while let Some(r) = rows.next()? {
2140 id_to_path.insert(r.get(0)?, r.get(1)?);
2141 }
2142 }
2143 let path_of = |id: i64| -> String {
2144 id_to_path.get(&id).cloned().unwrap_or_default()
2145 };
2146
2147 let mut phrase_to_non_def_files: FxHashMap<String, Vec<i64>> = FxHashMap::default();
2149 for (file_id, pairs) in &file_to_phrases {
2150 for (phrase, is_def) in pairs {
2151 if !*is_def {
2152 phrase_to_non_def_files
2153 .entry(phrase.clone())
2154 .or_default()
2155 .push(*file_id);
2156 }
2157 }
2158 }
2159
2160 for sym in symbols {
2162 let mut refs: HashSet<String> = HashSet::new();
2163
2164 let mut files_using_sym = match phrase_to_non_def_files.get(&sym.name) {
2165 Some(f) => f.clone(),
2166 None => continue,
2167 };
2168 files_using_sym.sort_by_key(|id| path_of(*id));
2174
2175 for &file_id in &files_using_sym {
2176 if let Some(pairs) = file_to_phrases.get(&file_id) {
2177 for (phrase, is_def) in pairs {
2178 if *is_def
2179 && phrase != &sym.name
2180 && def_names.contains(phrase.as_str())
2181 && !is_common_word(phrase)
2182 && phrase.len() >= 3
2183 && phrase.len() < 30
2184 && !phrase.starts_with('_')
2185 && !phrase.ends_with("Error")
2186 && !phrase.ends_with("Result")
2187 && !phrase.ends_with("Option")
2188 && phrase != "Self"
2189 {
2190 refs.insert(phrase.clone());
2191 }
2192 }
2193 }
2194
2195 if refs.len() >= 5 {
2197 break;
2198 }
2199 }
2200
2201 if !refs.is_empty() {
2203 let mut sorted: Vec<String> = refs.into_iter().collect();
2204 sorted.sort();
2205 sorted.truncate(3);
2206 ref_set.insert(sym.name.clone(), sorted.into_iter().collect());
2207 }
2208 }
2209
2210 for (name, refs) in ref_set {
2212 let mut sorted: Vec<String> = refs.into_iter().collect();
2213 sorted.sort();
2214 sorted.truncate(3);
2215 cross_refs.insert(name, sorted);
2216 }
2217
2218 Ok(cross_refs)
2219}
2220
2221fn read_symbols_for_subset(
2229 selected: &[Symbol],
2230 _all_symbols: &[Symbol],
2231) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2232 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2233 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2234
2235 for sym in selected {
2239 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2240 meta.lines.to_vec()
2241 } else {
2242 std::fs::read_to_string(&sym.file)
2244 .map(|c| c.lines().map(String::from).collect())
2245 .unwrap_or_default()
2246 };
2247
2248 let start = (sym.line as usize).min(lines.len().saturating_sub(1));
2249 let end = find_body_end(lines.as_slice(), start, sym.indent).min(lines.len());
2250 if start < end {
2251 for line in &lines[start..end] {
2253 let trimmed = line.trim();
2254 if trimmed.is_empty() {
2255 continue;
2256 }
2257 let skel = aggressive_skeleton(trimmed);
2258 if !skel.is_empty() {
2259 *freq.entry(skel).or_insert(0) += 1;
2260 }
2261 }
2262 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2263 }
2264 }
2265
2266 (sources, freq)
2267}
2268
2269fn read_symbols_and_frequency(
2272 symbols: &[Symbol],
2273) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2274 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2275 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2276
2277 for sym in symbols {
2280 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2281 meta.lines.to_vec()
2282 } else {
2283 std::fs::read_to_string(&sym.file)
2284 .map(|c| c.lines().map(String::from).collect())
2285 .unwrap_or_default()
2286 };
2287
2288 let start = (sym.line.max(0) as usize).min(lines.len().saturating_sub(1));
2293 let brace_end = find_body_end(lines.as_slice(), start, sym.indent);
2294 let index_end = (sym.end_line as usize).max(start + 1).min(lines.len());
2295 let end = brace_end.max(index_end).min(lines.len());
2296
2297 if start < end {
2298 for line in &lines[start..end] {
2300 let trimmed = line.trim();
2301 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
2302 continue;
2303 }
2304 let skel = aggressive_skeleton(trimmed);
2305 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 5 {
2306 continue;
2307 }
2308 *freq.entry(skel).or_insert(0) += 1;
2309 }
2310
2311 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2312 }
2313 }
2314
2315 (sources, freq)
2316}
2317
2318fn find_body_end(lines: &[String], start: usize, _base_indent: i32) -> usize {
2323 if start >= lines.len() {
2324 return start;
2325 }
2326 let first_line = &lines[start];
2327
2328 let stripped = first_line.trim();
2330 if stripped.starts_with("def ")
2331 || stripped.starts_with("class ")
2332 || stripped.starts_with("async def ")
2333 {
2334 let first_indent = first_line.len() - first_line.trim_start().len();
2335 for (i, line) in lines.iter().enumerate().skip(start + 1) {
2336 let trimmed = line.trim();
2337 if trimmed.is_empty() || trimmed.starts_with("#") {
2338 continue;
2339 }
2340 let indent = line.len() - line.trim_start().len();
2341 if indent <= first_indent
2343 && (trimmed.starts_with("def ")
2344 || trimmed.starts_with("class ")
2345 || trimmed.starts_with("async def "))
2346 {
2347 return i;
2348 }
2349 }
2350 return lines.len();
2351 }
2352
2353 let mut depth: i32 = 0;
2355 let mut seen_open = false;
2356 for (i, line) in lines.iter().enumerate().skip(start) {
2357 let trimmed = line.trim();
2358
2359 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
2361 continue;
2362 }
2363
2364 for ch in line.chars() {
2365 match ch {
2366 '{' => { depth += 1; seen_open = true; }
2367 '}' => { depth -= 1; }
2368 _ => {}
2369 }
2370 }
2371
2372 if seen_open && depth == 0 {
2373 return i + 1;
2374 }
2375 }
2376 lines.len()
2377}
2378
2379fn derive_crate_name(file_path: &str) -> String {
2386 let parts: Vec<&str> = file_path.split('/').collect();
2387 for (i, part) in parts.iter().enumerate() {
2388 if (*part == "crates" || *part == "packages") && i + 1 < parts.len() {
2389 return parts[i + 1].to_string();
2390 }
2391 }
2392 if parts.len() >= 2 {
2397 let parent = parts[parts.len() - 2];
2398 const GENERIC: &[&str] = &[
2399 "src", "lib", "app", "apps", "source", "sources", "test", "tests",
2400 "spec", "specs", "bin", "cmd", "main", "code", "internal", "pkg",
2401 ];
2402 if !GENERIC.contains(&parent) {
2403 return parent.to_string();
2404 }
2405 }
2406 String::new()
2407}
2408
2409fn detect_modules(symbols: &[Symbol]) -> Vec<(String, Vec<Symbol>)> {
2410 let mut module_map: FxHashMap<String, Vec<Symbol>> = FxHashMap::default();
2411
2412 for sym in symbols {
2413 let module = derive_module_name(&sym.file);
2414 module_map.entry(module).or_default().push(sym.clone());
2415 }
2416
2417 let mut modules: Vec<(String, Vec<Symbol>)> = module_map.into_iter().collect();
2418 modules.sort_by(|a, b| a.0.cmp(&b.0));
2419 modules
2420}
2421
2422fn derive_module_name(file_path: &str) -> String {
2423 let path = Path::new(file_path);
2424 let components: Vec<_> = path.components().collect();
2425
2426 for (i, comp) in components.iter().enumerate() {
2428 let part = comp.as_os_str().to_string_lossy().to_string();
2429 if part == "packages" && i + 1 < components.len() {
2430 return format!("packages/{}", components[i + 1].as_os_str().to_string_lossy());
2431 }
2432 if part == "src" && i + 1 < components.len() {
2433 return format!("src/{}", components[i + 1].as_os_str().to_string_lossy());
2434 }
2435 if part == "crates" && i + 1 < components.len() {
2436 return format!("crates/{}", components[i + 1].as_os_str().to_string_lossy());
2437 }
2438 }
2439
2440 components
2442 .first()
2443 .map(|c| c.as_os_str().to_string_lossy().to_string())
2444 .unwrap_or_else(|| "root".to_string())
2445}
2446
2447fn build_cochange_line(_name: &str, signature: &str, _body: &str, cross_refs: &[String]) -> String {
2453 let lower = signature.to_ascii_lowercase();
2454 let mut parts: Vec<String> = Vec::new();
2455 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2456 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2457
2458 if is_enum {
2459 if !cross_refs.is_empty() {
2460 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2461 parts.push(format!("Add variant → update match sites: {}", sites.join(", ")));
2462 }
2463 } else if is_struct {
2464 if !cross_refs.is_empty() {
2465 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2466 parts.push(format!("Add field → update constructors: {}", sites.join(", ")));
2467 }
2468 } else {
2469 if !cross_refs.is_empty() {
2470 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2471 parts.push(format!("Rename → update {} caller(s): {}", cross_refs.len(), sites.join(", ")));
2472 }
2473 }
2474 parts.join("; ")
2475}
2476
2477fn expand_type_signature(signature: &str, body: &str, def_line: i32) -> String {
2478 let sig_trimmed = signature.trim();
2479 let lower = sig_trimmed.to_ascii_lowercase();
2480 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2481 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2482 if !is_enum && !is_struct {
2483 return signature.to_string();
2484 }
2485 let mut names: Vec<String> = Vec::new();
2486 for (j, line) in body.lines().enumerate().skip(1) {
2487 let trimmed = line.trim();
2488 if trimmed == "}" || trimmed.is_empty() { continue; }
2489 if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with('*') {
2492 continue;
2493 }
2494 let abs_line = def_line + 1 + j as i32;
2498 if is_enum {
2499 let name = trimmed.split([',', '{', '(', ':']).next().unwrap_or("").trim().trim_end_matches(',');
2500 if !name.is_empty() && name.len() > 1 && name.chars().next().is_some_and(|c| c.is_uppercase() || c == '_') {
2501 names.push(format!("{} (line {})", name, abs_line));
2502 }
2503 } else if is_struct {
2504 if let Some(colon_pos) = trimmed.find(':') {
2505 let name = trimmed[..colon_pos].trim().trim_start_matches("pub ").trim();
2506 let ftype = trimmed[colon_pos + 1..]
2507 .trim()
2508 .trim_end_matches(',')
2509 .trim()
2510 .to_string();
2511 if !name.is_empty() && name.len() > 1 {
2512 if ftype.is_empty() || ftype == "{" {
2515 names.push(format!("{} (line {})", name, abs_line));
2516 } else {
2517 let ft = ftype.split('{').next().unwrap_or(&ftype).trim();
2518 names.push(format!("{}: {} (line {})", name, ft, abs_line));
2519 }
2520 }
2521 }
2522 }
2523 }
2524 if names.is_empty() { return signature.to_string(); }
2525 let kind = if is_enum { "variants" } else { "fields" };
2526 format!("{} // {}({})", sig_trimmed, kind, names.join(", "))
2527}
2528
2529fn render_entry(
2530 sym: &Symbol,
2531 body: &str,
2532 cross_refs: &[String],
2533 skeleton_freq: &FxHashMap<String, usize>,
2534 format: PackFormat,
2535) -> String {
2536 let crate_name = derive_crate_name(&sym.file);
2538 let header = if crate_name.is_empty() {
2539 format!("## {}", sym.name)
2540 } else {
2541 format!("## {}/{}", sym.name, crate_name)
2542 };
2543 let mut parts = vec![header];
2544
2545 if matches!(format, PackFormat::Full) && !sym.doc_comment.is_empty() {
2547 let l0 = sym
2548 .doc_comment
2549 .split('.')
2550 .next()
2551 .unwrap_or(&sym.doc_comment)
2552 .trim();
2553 let l0 = if l0.chars().count() > 120 {
2554 l0.chars().take(120).collect::<String>()
2556 } else {
2557 l0.to_string()
2558 };
2559 parts.push(format!("L0: {}", l0));
2560 }
2561
2562 let l2_line = expand_type_signature(&sym.signature, body, sym.line);
2564 parts.push(format!("L2: {} [{}:{}]", l2_line, sym.file, sym.line + 1));
2567
2568 if !body.is_empty() {
2570 let surprises = extract_surprise_from_body(body, skeleton_freq);
2571 if !surprises.is_empty() {
2572 parts.push(format!("L3: {}", surprises.join("; ")));
2573 }
2574 }
2575
2576 if matches!(format, PackFormat::Full) && cross_refs.len() >= 2 {
2578 parts.push(format!("Cross-refs: {}", cross_refs.iter().take(5).cloned().collect::<Vec<_>>().join(", ")));
2579 }
2580
2581 let l4 = build_cochange_line(&sym.name, &sym.signature, body, cross_refs);
2583 if !l4.is_empty() {
2584 parts.push(format!("L4: {}", l4));
2585 }
2586
2587 parts.push(String::new());
2588 parts.join("\n")
2589}
2590
2591#[cfg(test)]
2592mod noise_tests {
2593 use super::is_noise_line;
2594
2595 #[test]
2598 fn noise_classes_are_filtered() {
2599 let noise = [
2600 "",
2602 "}",
2603 "});",
2604 "};",
2605 "];",
2606 ")",
2607 "self.assertEqual(request.status, 200)",
2609 "self.assertIsNotNone(request.body)",
2610 "self.assertTrue(request.valid)",
2611 "block_phrases.append(request.text)",
2613 "results.append(item)",
2614 "log::info!(\"starting engine\")",
2616 "tracing::debug!(\"rpm = {}\", self.rpm)",
2617 "println!(\"done\")",
2618 "dbg!(value);",
2619 "#[derive(Debug, Clone, PartialEq)]",
2621 "derive(Serialize, Deserialize)",
2622 "break;",
2624 "continue;",
2625 "return None;",
2627 "return true;",
2628 "return false;",
2629 "let _ = do_thing();",
2631 "value.ok()?;",
2632 "thing.expect(\"must exist\");",
2633 "let x = y;",
2635 "let mut count = 0;",
2636 ];
2637 for line in noise {
2638 assert!(
2639 is_noise_line(line),
2640 "expected to be filtered as noise: {:?}",
2641 line
2642 );
2643 }
2644 }
2645
2646 #[test]
2648 fn signal_lines_are_preserved() {
2649 let signal = [
2650 "hash = hash.wrapping_mul(33).wrapping_add(byte);",
2651 "if total > threshold && !exhausted {",
2652 "match response_kind {",
2653 "let ratio = scored(matched, total) as f64;",
2654 "return Some(compute_fallback(input));",
2655 "buffer.extend_from_slice(&payload);",
2656 "channel.send(Message::Shutdown).await;",
2657 ];
2658 for line in signal {
2659 assert!(
2660 !is_noise_line(line),
2661 "expected to be preserved as signal: {:?}",
2662 line
2663 );
2664 }
2665 }
2666}
2667
2668#[cfg(test)]
2669mod v80_panic_tests {
2670 use super::*;
2671 #[test]
2672 fn manual_capture_sentinel_no_panic() {
2673 for t in ["is_empty", "x is_empty", "// is_empty", "if self.is_empty", "is_empty\nreturn x;"] {
2675 let _ = manual_capture_sentinel(t); }
2677 }
2678}