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 usage_files: Vec<i64> = file_to_phrases
415 .iter()
416 .filter(|(_, pairs)| pairs.iter().any(|(p, is_def)| *p == sym.name && !*is_def))
417 .map(|(fid, _)| *fid)
418 .take(50)
419 .collect();
420
421 let mut refs: HashSet<String> = HashSet::new();
422 for file_id in usage_files {
423 if let Some(pairs) = file_to_phrases.get(&file_id) {
424 for (phrase, is_def) in pairs {
425 if *is_def
426 && *phrase != sym.name
427 && all_names.contains(phrase.as_str())
428 && !is_common_word(phrase)
429 {
430 refs.insert(phrase.clone());
431 }
432 if refs.len() >= 6 {
433 break;
434 }
435 }
436 }
437 if refs.len() >= 6 {
438 break;
439 }
440 }
441
442 cross_refs.insert(sym.name.clone(), refs.into_iter().take(6).collect());
443 }
444
445 cross_refs
446}
447
448pub fn generate_pack_auto(path: &str) -> Result<String, String> {
454 let conn = open_index(path)?;
456 let decision = gate_decision_for(&conn, path)?;
457
458 match decision {
459 GateDecision::Skip => Ok(String::new()),
460 GateDecision::Minimal => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 15),
461 GateDecision::Full => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 50),
462 }
463}
464
465fn open_index(codebase_path: &str) -> Result<rusqlite::Connection, String> {
467 let db_path = format!(
468 "{}/.reliary/index.sqlite",
469 codebase_path.trim_end_matches('/')
470 );
471 let conn = Connection::open(&db_path)
472 .map_err(|e| format!("pack: open {}: {}", db_path, e))?;
473 reliary_search::schema::open_existing_db_safe(&conn)
474 .map_err(|e| format!("index not initialized: {}", e))?;
475 Ok(conn)
476}
477
478#[derive(Debug, Clone)]
480pub struct PackEntry {
481 pub name: String,
482 pub header: String, pub body: String, }
485
486pub fn parse_pack_entries(pack: &str) -> Vec<PackEntry> {
488 let mut entries = Vec::new();
489 let mut current_name: Option<String> = None;
490 let mut current_header: String = String::new();
491 let mut current_body: String = String::new();
492
493 for line in pack.lines() {
494 if line.starts_with("## ") {
495 if let Some(name) = current_name.take() {
497 entries.push(PackEntry {
498 name,
499 header: std::mem::take(&mut current_header),
500 body: std::mem::take(&mut current_body),
501 });
502 }
503 let after = line.strip_prefix("## ").unwrap_or(line);
507 current_name = Some(after.split('/').next().unwrap_or("").trim().to_string());
508 current_header = line.to_string();
509 } else if current_name.is_some() {
510 if !current_body.is_empty() {
511 current_body.push('\n');
512 }
513 current_body.push_str(line);
514 }
515 }
516 if let Some(name) = current_name {
518 entries.push(PackEntry {
519 name,
520 header: current_header,
521 body: current_body,
522 });
523 }
524 entries
525}
526
527fn tokenize(text: &str) -> Vec<String> {
531 reliary_search::tokenize(text)
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
548pub enum SliceDecision {
549 Slice,
551 Skip,
553}
554
555pub fn should_slice_for_query(query: &str) -> SliceDecision {
559 let text = query.to_ascii_lowercase();
560
561 if text.contains("bug") || text.contains("issue") || text.contains("wrong")
563 || text.contains("incorrect") || text.contains("missing") {
564 return SliceDecision::Slice;
565 }
566 if text.contains("call") || text.contains("caller") || text.contains("who uses")
567 || text.contains("references") || text.contains("called by") {
568 return SliceDecision::Slice;
569 }
570
571 if text.contains("value") || text.contains("threshold") || text.contains("parameter")
573 || text.contains("default") || text.contains("exactly") || text.contains("what number") {
574 return SliceDecision::Slice;
575 }
576
577 let func_indicators = [
581 "find_clusters", "find_clusters_global", "skelhash", "skeleton_hash",
582 "aggressive_skeleton_hash", "maxwell", "maxwellgate", "should_drop",
583 "detect_strategy", "detect_tabular", "detect_json", "callgraph",
584 "is_definition_line", "reliary_sift", "reliary-output",
585 ];
586 for indicator in &func_indicators {
587 if text.contains(indicator) {
588 return SliceDecision::Slice;
589 }
590 }
591
592 SliceDecision::Skip
595}
596
597pub fn adaptive_slice(pack: &str, query: &str, top_k: usize) -> (String, SliceDecision) {
600 let decision = should_slice_for_query(query);
601 match decision {
602 SliceDecision::Slice => (slice_pack_for_query(pack, query, top_k), decision),
603 SliceDecision::Skip => (String::new(), decision),
604 }
605}
606
607pub fn slice_pack_for_query(pack: &str, query: &str, top_k: usize) -> String {
611 let entries = parse_pack_entries(pack);
612 if entries.is_empty() {
613 return String::new();
614 }
615
616 let query_tokens = tokenize(query);
617 if query_tokens.is_empty() {
618 return entries
620 .into_iter()
621 .take(top_k)
622 .map(|e| format!("{}\n{}", e.header, e.body))
623 .collect::<Vec<_>>()
624 .join("\n\n");
625 }
626
627 let n_docs = entries.len() as f64;
629 let mut doc_tokens: Vec<Vec<String>> = Vec::with_capacity(entries.len());
630 for entry in &entries {
631 let combined = format!("{} {}", entry.name, entry.body);
633 doc_tokens.push(tokenize(&combined));
634 }
635
636 let mut df: FxHashMap<String, usize> = FxHashMap::default();
638 for tokens in &doc_tokens {
639 let unique: HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
640 for t in &query_tokens {
641 if unique.contains(t.as_str()) {
642 *df.entry(t.clone()).or_insert(0) += 1;
643 }
644 }
645 }
646
647 let total_len: usize = doc_tokens.iter().map(|d| d.len()).sum();
649 let avgdl = if n_docs > 0.0 {
650 total_len as f64 / n_docs
651 } else {
652 1.0
653 };
654
655 let mut scored: Vec<(f64, &PackEntry)> = Vec::with_capacity(entries.len());
657 for (i, entry) in entries.iter().enumerate() {
658 let doc_len = doc_tokens[i].len() as f64;
659 let mut score = 0.0;
660 let mut tf: FxHashMap<&str, usize> = FxHashMap::default();
662 for t in &doc_tokens[i] {
663 *tf.entry(t.as_str()).or_insert(0) += 1;
664 }
665 for qt in &query_tokens {
666 let d_freq = *df.get(qt).unwrap_or(&0) as f64;
667 if d_freq == 0.0 {
668 continue;
669 }
670 let idf = ((n_docs - d_freq + 0.5) / (d_freq + 0.5) + 1.0).ln();
671 let term_freq = *tf.get(qt.as_str()).unwrap_or(&0) as f64;
672 if term_freq > 0.0 {
673 let bm25 = idf * (term_freq * (1.2 + 1.0))
674 / (term_freq + 1.2 * (1.0 - 0.75 + 0.75 * (doc_len / avgdl)));
675 score += bm25;
676 }
677 }
678 let name_lower = entry.name.to_ascii_lowercase();
680 for qt in &query_tokens {
681 if name_lower == *qt {
682 score += 5.0; } else if name_lower.contains(qt.as_str()) {
684 score += 2.0; }
686 }
687 scored.push((score, entry));
688 }
689
690 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
692
693 let selected: Vec<&PackEntry> = scored
695 .into_iter()
696 .filter(|(s, _)| *s > 0.0)
697 .take(top_k)
698 .map(|(_, e)| e)
699 .collect();
700
701 if selected.is_empty() {
702 return String::new();
703 }
704
705 let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
710 let mut entries_by_name: FxHashMap<&str, &PackEntry> = FxHashMap::default();
712 for e in &entries {
713 entries_by_name.entry(e.name.as_str()).or_insert(e);
714 }
715 let mut expanded: Vec<&PackEntry> = selected.clone();
716 let mut expanded_names: HashSet<String> = selected_names.clone();
717 let mut expansion_count = 0;
718 let max_expansion = 15usize.saturating_sub(top_k.min(15));
719 for entry in &selected {
720 for line in entry.body.lines() {
721 let refs_text = if let Some(rest) = line.strip_prefix("Cross-refs:") {
722 Some(rest.trim())
723 } else if line.starts_with("L4:") && line.contains("caller") {
724 if let Some(pos) = line.find(": ") {
726 let after = &line[pos + 2..];
727 if let Some(last_colon) = after.rfind(": ") {
729 Some(&after[last_colon + 2..])
730 } else {
731 Some(after)
732 }
733 } else {
734 None
735 }
736 } else {
737 None
738 };
739 if let Some(refs) = refs_text {
740 for ref_name in refs.split(',').take(3) {
741 let ref_name = ref_name.trim();
742 if ref_name.is_empty() { continue; }
743 let ref_name_slash = format!("{}/", ref_name);
744 if let Some(e) = entries_by_name.get(ref_name).or_else(|| {
746 entries_by_name.values().find(|e| e.name.starts_with(&ref_name_slash))
747 }) {
748 if !selected_names.contains(&e.name) && !expanded_names.contains(&e.name) {
749 expanded_names.insert(e.name.clone());
750 expanded.push(e);
751 expansion_count += 1;
752 if expansion_count >= max_expansion { break; }
753 }
754 }
755 if expansion_count >= max_expansion { break; }
756 }
757 break; }
759 }
760 if expansion_count >= max_expansion { break; }
761 }
762
763 let total_entries = expanded.len();
764
765 let mut result = format!(
766 "# Sliced Pack — {} entries for query: \"{}\"\n\n",
767 total_entries,
768 query.chars().take(80).collect::<String>()
769 );
770 for entry in &expanded {
771 result.push_str(&format!("{}\n{}\n\n", entry.header, entry.body));
772 }
773 result
774}
775
776pub fn generate_hierarchical_pack(
778 path: &str,
779 format: PackFormat,
780 max_symbols_per_module: usize,
781) -> Result<HierarchicalPack, String> {
782 let db_path = format!(
783 "{}/.reliary/index.sqlite",
784 path.trim_end_matches('/')
785 );
786 let db = Connection::open(&db_path)
787 .map_err(|e| format!("cannot open index: {}", e))?;
788 reliary_search::schema::open_existing_db_safe(&db)
789 .map_err(|e| format!("index not initialized: {}", e))?;
790
791 let symbols = extract_symbols_from_index(&db)?;
792 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
793 let cross_refs = build_cross_refs_from_index(&db, &symbols);
794
795 let modules = detect_modules(&symbols);
797
798 let mut top_parts = vec![format!(
800 "# Holographic Pack — {} (top-level map)\n",
801 Path::new(path)
802 .file_name()
803 .map(|n| n.to_string_lossy().to_string())
804 .unwrap_or_else(|| path.to_string()),
805 )];
806
807 let mut module_packs: Vec<(String, String)> = Vec::new();
808
809 for (module_name, module_syms) in &modules {
810 let sym_count = module_syms.len();
811 let key_syms: Vec<String> = module_syms
812 .iter()
813 .filter(|s| !s.doc_comment.is_empty())
814 .take(5)
815 .map(|s| s.name.clone())
816 .collect();
817
818 top_parts.push(format!("## {}", module_name));
819 top_parts.push(format!("L0: {} symbols across this module", sym_count));
820 if !key_syms.is_empty() {
821 top_parts.push(format!("Key symbols: {}", key_syms.join(", ")));
822 }
823 top_parts.push(String::new());
824
825 let limited: Vec<&Symbol> = module_syms.iter().take(max_symbols_per_module).collect();
827 if limited.is_empty() {
828 continue;
829 }
830 let mut pack_parts = vec![format!("# Holographic Pack — {}\n", module_name)];
831 for sym in limited {
832 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
833 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
834 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
835 pack_parts.push(entry);
836 }
837 module_packs.push((module_name.clone(), pack_parts.join("\n")));
838 }
839
840 Ok(HierarchicalPack {
841 top_level: top_parts.join("\n"),
842 module_packs,
843 })
844}
845
846pub struct HierarchicalPack {
847 pub top_level: String,
848 pub module_packs: Vec<(String, String)>,
849}
850
851#[derive(Debug, Clone)]
856pub struct Symbol {
857 pub name: String,
858 pub signature: String,
859 pub file: String,
860 pub line: i32,
861 pub doc_comment: String,
862 pub block_id: i64,
863 pub end_line: i32,
864 pub indent: i32,
865}
866
867fn extract_symbols_from_index(db: &Connection) -> Result<Vec<Symbol>, String> {
872 let mut stmt = db
873 .prepare(
874 "SELECT o.phrase_id, o.file_id, o.line, o.block_id,
875 p.phrase, f.file_path,
876 b.end_line, b.indent
877 FROM occurrence o
878 JOIN phrases p ON o.phrase_id = p.id
879 JOIN file_map f ON o.file_id = f.id
880 JOIN block b ON o.block_id = b.block_id
881 WHERE o.is_def = 1
882 ORDER BY f.file_path, o.line",
883 )
884 .map_err(|e| format!("query symbols: {}", e))?;
885
886 let rows = stmt
887 .query_map([], |row| {
888 let phrase: String = row.get(4)?;
889 let file_path: String = row.get(5)?;
890 let line: i32 = row.get(2)?;
891 let block_id: i64 = row.get(3)?;
892 let end_line: i32 = row.get(6)?;
893 let indent: i32 = row.get(7)?;
894 Ok((phrase, file_path, line, block_id, end_line, indent))
895 })
896 .map_err(|e| format!("query symbols: {}", e))?;
897
898 let mut symbols: Vec<Symbol> = Vec::new();
899 let mut seen: HashSet<(String, String)> = HashSet::new();
903
904 let mut lines_cache: FxHashMap<String, Vec<String>> = FxHashMap::default();
907
908 for row in rows {
909 let (_phrase, file_path, line, block_id, end_line, indent) =
910 row.map_err(|e| format!("row: {}", e))?;
911
912 if !is_source_file(&file_path) {
914 continue;
915 }
916
917 if !lines_cache.contains_key(&file_path) {
919 let lines_vec: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&file_path) {
920 meta.lines.clone()
921 } else {
922 std::fs::read_to_string(&file_path)
923 .map(|c| c.lines().map(String::from).collect())
924 .unwrap_or_default()
925 };
926 lines_cache.insert(file_path.clone(), lines_vec);
927 }
928
929 let lines = &lines_cache[&file_path];
930
931 let signature = read_signature_line(lines, line);
933
934 let ends_with_brace = signature.trim_end().ends_with('{');
941 let classified =
942 reliary_search::structural::classify_structural(&signature, 0, ends_with_brace, false);
943 let real_name = if classified.is_def {
944 classified.defined_name.map(|s| s.to_string()).unwrap_or_default()
945 } else if is_definition_like(&signature) {
946 extract_name_from_signature(&signature)
950 } else {
951 String::new()
952 };
953 if real_name.len() < 3 {
954 continue;
955 }
956
957 if is_common_word(&real_name) {
959 continue;
960 }
961
962 if real_name.starts_with("__") && real_name.ends_with("__") {
964 continue;
965 }
966
967 let key = (real_name.clone(), file_path.clone());
969 if seen.contains(&key) {
970 continue;
971 }
972 seen.insert(key);
973
974 let doc_comment = read_doc_comment(lines, line);
976
977 symbols.push(Symbol {
978 name: real_name,
979 signature,
980 file: file_path,
981 line,
982 doc_comment,
983 block_id,
984 end_line,
985 indent,
986 });
987 }
988
989 Ok(symbols)
990}
991
992fn extract_name_from_signature(sig: &str) -> String {
997 let cleaned = sig.trim();
998
999 let mut rest = cleaned;
1001 let keywords = [
1002 "pub(crate) ", "pub(super) ", "pub ", "export ", "async ", "extern ",
1003 "static ", "inline ", "virtual ", "override ", "final ", "abstract ",
1004 "unsafe ", "mut ",
1005 ];
1006 loop {
1007 let mut changed = false;
1008 for kw in &keywords {
1009 if rest.starts_with(kw) {
1010 rest = &rest[kw.len()..];
1011 changed = true;
1012 break;
1013 }
1014 }
1015 if !changed {
1016 break;
1017 }
1018 }
1019
1020 let decl_kws = [
1022 "fn ", "function ", "def ", "func ", "struct ", "class ", "enum ",
1023 "trait ", "interface ", "type ", "impl ", "const ", "let ", "var ",
1024 "val ",
1025 ];
1026 for kw in &decl_kws {
1027 if rest.starts_with(kw) {
1028 rest = &rest[kw.len()..];
1029 break;
1030 }
1031 }
1032
1033 if rest.starts_with("impl ") {
1036 rest = &rest[5..];
1037 }
1038
1039 let end = rest
1041 .find(['(', '<', ':', '{', ' ', '=', ';'])
1042 .unwrap_or(rest.len());
1043 let name = rest[..end].trim();
1044
1045 let name = name.trim_start_matches('\'');
1047
1048 if name.is_empty() || name.len() < 2 {
1049 return String::new();
1050 }
1051
1052 if !name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
1054 return String::new();
1055 }
1056 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1057 return String::new();
1058 }
1059
1060 name.to_string()
1061}
1062
1063fn is_source_file(path: &str) -> bool {
1065 if path.contains("/bench/")
1068 || path.contains("/tests/")
1069 || path.contains("/test/")
1070 || path.contains("/target/")
1071 || path.contains("/vendor/")
1072 || path.contains("/node_modules/")
1073 || path.contains("/__pycache__/")
1074 || path.contains("/.git/")
1075 {
1076 return false;
1077 }
1078 let ext = Path::new(path)
1079 .extension()
1080 .and_then(|e| e.to_str())
1081 .unwrap_or("");
1082 matches!(
1088 ext,
1089 "rs" | "py" | "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs"
1090 | "go" | "java" | "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hh"
1091 | "rb" | "swift" | "kt" | "kts" | "scala" | "clj" | "ex" | "exs"
1092 | "elm" | "fs" | "fsx" | "ml" | "mli" | "hs" | "lua" | "php" | "pl"
1093 | "cs" | "dart" | "groovy" | "m" | "mm" | "jl" | "r" | "v"
1094 )
1095}
1096
1097fn is_definition_like(line: &str) -> bool {
1101 let stripped = line.trim();
1102 if stripped.is_empty() {
1103 return false;
1104 }
1105
1106 if stripped.starts_with("//")
1108 || stripped.starts_with('#')
1109 || stripped.starts_with("/*")
1110 || stripped.starts_with("///")
1111 || stripped.starts_with("--")
1112 || stripped.starts_with('\'')
1113 {
1114 return false;
1115 }
1116
1117 if stripped.starts_with("if ")
1119 || stripped.starts_with("for ")
1120 || stripped.starts_with("while ")
1121 || stripped.starts_with("return ")
1122 || stripped.starts_with("match ")
1123 || stripped.starts_with("switch ")
1124 || stripped.starts_with("let ")
1125 || stripped.starts_with("let mut")
1126 || stripped.starts_with("const ")
1127 || stripped.starts_with("var ")
1128 || stripped.starts_with("static ") || stripped.starts_with("type ") || stripped.starts_with("enum ") || stripped.starts_with("f.write")
1132 || stripped.starts_with("println")
1133 || stripped.starts_with("print")
1134 || stripped.starts_with("eprintln")
1135 {
1136 return false;
1137 }
1138
1139 let check = stripped.trim_end_matches([';', ',']);
1140
1141 let ends_with_brace = check.ends_with('{')
1153 || (check.ends_with('}')
1154 && check.matches('{').count() == 1
1155 && check.matches('}').count() == 1);
1156 if check.contains('(') && ends_with_brace {
1157 let before_paren = check.split('(').next().unwrap_or("").trim();
1158 let mut name_part = before_paren;
1162 for kw in &["pub(crate) ", "pub(super) ", "pub ", "async ", "unsafe ",
1163 "extern ", "fn ", "function ", "func ", "export ",
1164 "static ", "inline ", "const ", "final ", "override "] {
1165 if name_part.starts_with(kw) {
1166 name_part = &name_part[kw.len()..];
1167 }
1168 }
1169 if !name_part.contains("::")
1172 && !name_part.contains('.')
1173 && !name_part.contains("->")
1174 && !name_part.contains("||")
1175 && !name_part.contains('=')
1176 && !name_part.contains(' ')
1177 && name_part.len() > 3
1178 {
1179 return true;
1180 }
1181 }
1182
1183 if check.starts_with("def ") && check.ends_with(':') {
1185 return true;
1186 }
1187
1188 if check.ends_with('{') {
1190 let lower = check.to_ascii_lowercase();
1191 if lower.contains(" struct ")
1192 || lower.contains(" class ")
1193 || lower.contains(" enum ")
1194 || lower.contains(" trait ")
1195 || lower.contains(" interface ")
1196 || lower.contains(" impl ")
1197 || lower.starts_with("struct ")
1198 || lower.starts_with("class ")
1199 || lower.starts_with("enum ")
1200 || lower.starts_with("trait ")
1201 || lower.starts_with("interface ")
1202 || lower.starts_with("impl ")
1203 || lower.starts_with("pub struct ")
1204 || lower.starts_with("pub enum ")
1205 || lower.starts_with("pub trait ")
1206 {
1207 return true;
1208 }
1209 }
1210
1211 if check.ends_with('{') && (check.starts_with("export interface ") || check.starts_with("interface ") || check.starts_with("export type ") || check.starts_with("type ")) {
1213 return true;
1214 }
1215
1216 false
1217}
1218
1219fn is_common_word(word: &str) -> bool {
1221 if word.len() < 3 {
1223 return true;
1224 }
1225 const COMMON: &[&str] = &[
1227 "the", "and", "for", "not", "are", "but", "you", "all", "can", "her",
1228 "was", "one", "our", "out", "has", "his", "how", "its", "may", "new",
1229 "now", "old", "see", "way", "who", "did", "get", "let", "say", "she",
1230 "too", "use", "any", "ask", "bad", "big", "day", "end", "few", "got",
1231 "had", "him", "his", "how", "its", "man", "men", "put", "run", "set",
1232 "try", "two", "use", "via", "yet", "also", "code", "data", "file",
1233 "from", "have", "into", "just", "like", "make", "more", "must",
1234 "only", "over", "some", "such", "than", "them", "then", "they",
1235 "this", "that", "when", "what", "where", "which", "while", "will",
1236 "with", "work", "would", "your", "about", "after", "again", "before",
1237 "being", "below", "could", "every", "first", "found", "great",
1238 "group", "having", "here", "high", "into", "itself", "just",
1239 "large", "last", "left", "like", "long", "made", "many", "most",
1240 "much", "never", "next", "once", "other", "part", "place", "right",
1241 "same", "should", "show", "since", "small", "some", "still",
1242 "such", "take", "their", "them", "then", "there", "these",
1243 "they", "thing", "think", "this", "those", "through", "time",
1244 "under", "until", "very", "well", "were", "what", "when",
1245 "where", "which", "while", "will", "with", "without", "would",
1246 "write", "your", "index", "free", "grammar", "instead", "tool",
1247 "tree", "language", "read", "work", "reliary",
1248 ];
1249 static COMMON_SET: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
1251 let set = COMMON_SET.get_or_init(|| std::collections::HashSet::from_iter(COMMON.iter().copied()));
1252 let lower = word.to_ascii_lowercase();
1253 set.contains(lower.as_str())
1254}
1255
1256fn read_signature_line(lines: &[String], line: i32) -> String {
1257 lines
1258 .get(line.max(0) as usize)
1259 .map(|s| s.trim().to_string())
1260 .unwrap_or_default()
1261}
1262
1263fn read_doc_comment(lines: &[String], def_line: i32) -> String {
1264 let def_idx = (def_line.max(0) as usize).min(lines.len().saturating_sub(1));
1265
1266 let mut doc_parts: Vec<String> = Vec::new();
1268 let mut j = def_idx.saturating_sub(1);
1269
1270 if j < lines.len() && lines[j].trim().ends_with("*/") {
1272 let mut block: Vec<String> = Vec::new();
1273 while j > 0 {
1274 let l = lines[j].trim();
1275 block.insert(0, l.to_string());
1276 if l.starts_with("/**") || l.starts_with("/*") {
1277 break;
1278 }
1279 j -= 1;
1280 }
1281 for bl in &block {
1282 let clean = bl
1283 .trim()
1284 .trim_start_matches("/**")
1285 .trim_start_matches("/*")
1286 .trim_end_matches("*/")
1287 .trim()
1288 .trim_start_matches('*')
1289 .trim();
1290 if !clean.is_empty() && !clean.starts_with('@') && clean != "/**" {
1291 doc_parts.insert(0, clean.to_string());
1292 }
1293 }
1294 }
1295
1296 if doc_parts.is_empty() {
1297 j = def_idx.saturating_sub(1);
1299 while j > 0 {
1300 let l = lines[j].trim();
1301 if let Some(rest) = l.strip_prefix("///") {
1302 doc_parts.insert(0, rest.trim().to_string());
1303 } else if let Some(rest) = l.strip_prefix("//") {
1304 doc_parts.insert(0, rest.trim().to_string());
1305 } else if l.starts_with('#') && !l.starts_with("#[") {
1306 doc_parts.insert(0, l[1..].trim().to_string());
1307 } else if l.is_empty() {
1308 } else {
1310 break;
1311 }
1312 j -= 1;
1313 }
1314 }
1315
1316 doc_parts.join(" ")
1317}
1318
1319fn is_noise_line(line: &str) -> bool {
1331 let trimmed = line.trim();
1332 if trimmed.is_empty() {
1333 return true;
1334 }
1335 if trimmed.len() < 15 {
1338 return true;
1339 }
1340 if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1342 return true;
1343 }
1344 if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1347 return true;
1348 }
1349 if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1351 return true;
1352 }
1353 if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1355 return true;
1356 }
1357 if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1359 return true;
1360 }
1361 if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1363 return true;
1364 }
1365 if trimmed.starts_with("assert") {
1367 return true;
1368 }
1369 if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1371 return true;
1372 }
1373 if (trimmed.starts_with("let ") || trimmed.starts_with("let mut "))
1377 && !trimmed.contains("(")
1378 && !trimmed.contains("{")
1379 && trimmed.ends_with(";")
1380 {
1381 return true;
1382 }
1383 let trimmed_for_return = if let Some(comment_start) = trimmed.find("//") {
1387 trimmed[..comment_start].trim()
1388 } else {
1389 trimmed
1390 };
1391 if let Some(rest) = trimmed_for_return.strip_prefix("return ") {
1392 if let Some(inner) = rest.strip_suffix(";") {
1395 let inner = inner.trim();
1396 if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1397 return true;
1399 }
1400 }
1401 if rest.contains(".lower()") || rest.contains(".upper()") {
1403 return true;
1404 }
1405 }
1406 if trimmed.starts_with("if isinstance(") {
1408 return true;
1409 }
1410 if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1412 return true;
1413 }
1414 if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1416 return true;
1417 }
1418 if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1420 return true;
1421 }
1422 if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1424 return true;
1425 }
1426 if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1428 return true;
1429 }
1430 if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1433 return true;
1434 }
1435 if let Some(rest) = trimmed.strip_prefix("self.") {
1437 if rest.starts_with("assert") || rest.contains(".assert") || rest.starts_with("setUp")
1438 || rest.starts_with("tearDown") || rest.starts_with("set_up")
1439 || rest.starts_with("tear_down") || rest.starts_with("skip(")
1440 || rest.starts_with("fixture") || rest.starts_with("patch(")
1441 {
1442 return true;
1443 }
1444 }
1445 if trimmed.contains(".append(") || trimmed.contains(".push(")
1448 || trimmed.contains(".insert(") || trimmed.contains(".remove(")
1449 || trimmed.contains(".clear(") || trimmed.contains(".drop(")
1450 || trimmed.contains(".len()") || trimmed.contains(".is_empty()")
1451 || trimmed.contains(".expect(") || trimmed.contains(".collect()")
1452 || trimmed.contains(".clone(") || trimmed.contains(".iter()")
1453 {
1454 return true;
1455 }
1456 if trimmed == "break;" || trimmed == "continue;"
1458 || trimmed == "break" || trimmed == "continue" {
1459 return true;
1460 }
1461 if trimmed.starts_with("from ") {
1463 return true;
1464 }
1465 if let Some(stripped_return) = trimmed.strip_prefix("return ") {
1467 let rest = stripped_return.trim().trim_end_matches(';').trim();
1468 if matches!(rest, "true" | "false" | "None" | "0" | "1" | "Self" | "-1") {
1469 return true;
1470 }
1471 }
1472 if trimmed.starts_with("log::") || trimmed.starts_with("tracing::")
1474 || trimmed.starts_with("debug!") || trimmed.starts_with("info!")
1475 || trimmed.starts_with("warn!") || trimmed.starts_with("error!")
1476 || trimmed.starts_with("print!") || trimmed.starts_with("eprint!")
1477 || trimmed.starts_with("println!") || trimmed.starts_with("eprintln!")
1478 || trimmed.starts_with("dbg!") || trimmed.starts_with("todo!")
1479 {
1480 return true;
1481 }
1482 if trimmed.starts_with("let _ =") || trimmed.starts_with("let _mut _ =")
1484 || trimmed.starts_with("let (_, _) =") || trimmed.starts_with("let (_, _, _) =")
1485 {
1486 return true;
1487 }
1488 let code_only = if let Some(comment_start) = trimmed.find("//") {
1491 trimmed[..comment_start].trim()
1492 } else {
1493 trimmed
1494 };
1495 if code_only.ends_with(".ok()?") || code_only.ends_with(".ok();")
1496 || code_only.contains(".expect(\"") || code_only.contains(".unwrap_or(")
1497 || code_only.contains(".unwrap_or_default(") || code_only.contains(".unwrap_or_else(")
1498 || (code_only.starts_with("return ") && code_only.contains(".ok()"))
1499 || (code_only.starts_with("return ") && code_only.contains(".err()"))
1500 || (code_only.starts_with("return ") && code_only.contains(".unwrap()"))
1501 {
1502 return true;
1503 }
1504 if let Some(eq_pos) = trimmed.find('=') {
1505 if eq_pos > 0 && !trimmed[eq_pos+1..].trim().starts_with("\"")
1506 && trimmed.contains(".to_string(")
1507 && !trimmed.contains("unsafe ") && !trimmed.contains("transmute")
1508 {
1509 return true;
1510 }
1511 }
1512 false
1513}
1514
1515
1516fn extract_surprise_from_body(
1522 body: &str,
1523 skeleton_freq: &FxHashMap<String, usize>,
1524) -> Vec<String> {
1525 let mut surprises: Vec<String> = Vec::new();
1526 let mut seen: HashSet<String> = HashSet::new();
1527
1528 let lines: Vec<&str> = body.lines().collect();
1529
1530 if let Some(m) = regex_capture(r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{", body) {
1537 add_surprise(&mut surprises, &mut seen,
1538 format!("auto-pass for input below {} chars (no gates applied)", m));
1539 }
1540
1541 if body.contains("expect_dash") || body.contains("j == 8") && body.contains("j == 13") && body.contains("j == 23") {
1544 add_surprise(&mut surprises, &mut seen,
1545 "UUID dash positions: 8, 13, 18, 23 (standard 8-4-4-4-12)".to_string());
1546 }
1547
1548 if body.contains("he - i") || body.contains("hex_len") {
1551 if let Some(m) = regex_capture(r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)", body) {
1552 add_surprise(&mut surprises, &mut seen,
1553 format!("hex-hash length range: {} to {}", m.split(',').next().unwrap_or(""), m.split(',').nth(1).unwrap_or("")));
1554 }
1555 }
1556
1557 if body.contains("5381") && body.contains("wrapping_mul") && body.contains("33") {
1560 add_surprise(&mut surprises, &mut seen,
1561 "djb3-33 hash: starts at 5381, multiplies by 33 (Bernstein variant)".to_string());
1562 }
1563
1564 if body.contains("bytes[") && body.contains("while") && !body.contains("chars()") {
1567 add_surprise(&mut surprises, &mut seen,
1568 "byte-indexed processing (not char-indexed)".to_string());
1569 }
1570
1571 if let Some(m) = regex_capture(r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)", body) {
1574 add_surprise(&mut surprises, &mut seen,
1575 format!("sentinel return: {} for empty input", m));
1576 }
1577
1578if body.contains("import") && body.contains("count") && body.contains('[') {
1581 add_surprise(&mut surprises, &mut seen,
1582 "imports collapsed into a single label '[N imports]'".to_string());
1583 }
1584
1585 if body.contains("flush") || (body.contains("if ") && body.contains("emit") && body.contains("reset")) {
1588 add_surprise(&mut surprises, &mut seen,
1589 "lazy flush: label emitted on next non-matching line, not immediately".to_string());
1590 }
1591
1592 if body.contains("Error") && body.contains("Comment") {
1595 let error_pos = body.find("Error").unwrap_or(0);
1596 let comment_pos = body.find("Comment").unwrap_or(0);
1597 if error_pos < comment_pos && error_pos > 0 && comment_pos > 0 {
1598 add_surprise(&mut surprises, &mut seen,
1599 "Error check runs BEFORE Comment check (priority order)".to_string());
1600 } else if comment_pos < error_pos && comment_pos > 0 && error_pos > 0 {
1601 add_surprise(&mut surprises, &mut seen,
1602 "Comment check runs BEFORE Error check (potential bug: errors in comments are missed)".to_string());
1603 }
1604 }
1605
1606 if regex_capture(r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+", body).is_some() {
1609 if body.contains("&&") {
1610 add_surprise(&mut surprises, &mut seen,
1611 "gates are AND-combined: ALL must fail for None return".to_string());
1612 } else if body.contains("||") {
1613 add_surprise(&mut surprises, &mut seen,
1614 "gates are OR-combined: ANY failing gate returns None".to_string());
1615 }
1616 }
1617
1618 if body.contains("aggressive") || body.contains("collapse") {
1621 if let Some(m) = regex_capture(r"\w+\s*>\s*\w+\s*\+\s*(\d+)", body) {
1622 add_surprise(&mut surprises, &mut seen,
1623 format!("minimum word length threshold: {} (below this, words kept verbatim)", m));
1624 }
1625 }
1626
1627 if let Some(m) = regex_capture(r"let\s+(\w+)\s*=\s*skeleton\(\w+\)", body) {
1630 add_surprise(&mut surprises, &mut seen,
1631 format!("operates on the SKELETON of input, not the original text (via {})", m));
1632 }
1633
1634 if body.contains("replace_all") && body.contains("&") {
1637 let placeholders: Vec<&str> = body
1638 .split(".replace_all")
1639 .skip(1)
1640 .filter_map(|s| {
1641 if let Some(idx) = s.find('"') {
1642 let after = &s[idx..];
1643 if let Some(end) = after[1..].find('"') {
1644 return Some(&after[1..=end]);
1645 }
1646 }
1647 None
1648 })
1649 .collect();
1650 if !placeholders.is_empty() {
1651 add_surprise(&mut surprises, &mut seen,
1652 format!("regex-based replacement: {}", placeholders.iter().take(3).cloned().collect::<Vec<_>>().join(", ")));
1653 }
1654 }
1655
1656if body.contains("num_end") && body.contains('.') {
1659 add_surprise(&mut surprises, &mut seen,
1660 "version detection: requires exactly three dot-separated numeric components (X.Y.Z)".to_string());
1661 }
1662
1663 let mut seen_nums: HashSet<String> = HashSet::new();
1665 let mut seen_conds: HashSet<String> = HashSet::new();
1666
1667 for (line_idx, line) in lines.iter().enumerate() {
1668 let trimmed = line.trim();
1669 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#")
1670 || trimmed.starts_with("///") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
1671 continue;
1672 }
1673 let code = trimmed;
1674
1675 for num in find_magic_numbers(code) {
1677 let key = format!("{}_{}", num, line_idx);
1678 if !seen_nums.contains(&key) {
1679 seen_nums.insert(key);
1680 let labeled = label_number(&num, code);
1681 if !labeled.starts_with("constant: 0") && !labeled.starts_with("constant: 1") {
1682 add_surprise(&mut surprises, &mut seen, labeled);
1683 }
1684 }
1685 }
1686
1687 if code.contains("if ") && (code.contains("&&") || code.contains("||")) {
1689 if let Some(cond) = extract_condition(code) {
1690 let cond_key: String = cond.chars().take(40).collect();
1691 if cond.len() > 10 && cond.len() < 200 && !seen_conds.contains(&cond_key) {
1692 seen_conds.insert(cond_key);
1693 add_surprise(&mut surprises, &mut seen,
1694 format!("compound condition: {}", cond));
1695 }
1696 }
1697 }
1698
1699 if line_idx < 15 && code.contains("if ") && code.contains("return") {
1701 if let Some(ret) = extract_return_value(code) {
1702 if !ret.is_empty() && ret != "true" && ret != "false" {
1703 add_surprise(&mut surprises, &mut seen,
1704 format!("early return → {}", ret));
1705 }
1706 }
1707 }
1708
1709 if code.contains(".wrapping_") {
1711 let ops: Vec<&str> = code
1712 .split(".wrapping_")
1713 .skip(1)
1714 .filter_map(|s| s.split('(').next())
1715 .collect();
1716 if !ops.is_empty() {
1717 add_surprise(&mut surprises, &mut seen,
1718 format!("wrapping: {}", ops.join(", ")));
1719 }
1720 }
1721 if code.contains("unwrap_or(") {
1722 if let Some(def) = extract_unwrap_default(code) {
1723 if !def.is_empty() {
1724 add_surprise(&mut surprises, &mut seen,
1725 format!("fallback: unwrap_or({})", def));
1726 }
1727 }
1728 }
1729
1730 if code.contains("with_capacity(") {
1732 if let Some(n) = extract_capacity(code) {
1733 add_surprise(&mut surprises, &mut seen,
1734 format!("capacity: {}", n));
1735 }
1736 }
1737
1738 if let Some(comment_start) = code.find("//") {
1740 let comment = code[comment_start + 2..].trim().to_ascii_lowercase();
1741 let comment_text = code[comment_start + 2..].trim();
1742 for sig in ["key", "difference", "note", "important", "surprise",
1743 "critical", "deliberate", "only fires", "not regex", "must"]
1744 {
1745 if comment.contains(sig) {
1746 add_surprise(&mut surprises, &mut seen,
1747 format!("source note: {}", comment_text));
1748 break;
1749 }
1750 }
1751 }
1752 }
1753
1754 for (line_idx, line) in lines.iter().enumerate() {
1756 if line_idx == 0 { continue; } let trimmed = line.trim();
1758 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
1759 continue;
1760 }
1761 if is_noise_line(trimmed) {
1762 continue;
1763 }
1764 let skel = aggressive_skeleton(trimmed);
1765 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 10 {
1766 continue;
1767 }
1768 let freq = skeleton_freq.get(&skel).copied().unwrap_or(0);
1769 if freq == 1 {
1770 let has_signal = trimmed.contains('(')
1775 || trimmed.contains("==") || trimmed.contains("!=")
1776 || trimmed.contains(" < ") || trimmed.contains(" > ")
1777 || trimmed.contains(">=") || trimmed.contains("<=")
1778 || trimmed.contains("&&") || trimmed.contains("||")
1779 || trimmed.contains("wrapping_") || trimmed.contains("unwrap_or")
1780 || trimmed.contains("match ") || trimmed.contains("=>");
1781 if has_signal {
1782 let snippet: String = trimmed.chars().take(80).collect();
1783 add_surprise(&mut surprises, &mut seen,
1784 format!("unique line: {}", snippet));
1785 }
1786 }
1787 }
1788
1789 let pre_dedup_len = surprises.len();
1792 let mut final_surprises: Vec<String> = Vec::new();
1793 let mut seen_fragments: HashSet<String> = HashSet::new();
1794 for s in surprises {
1795 let fragment: String = s
1797 .chars()
1798 .filter(|c| !c.is_whitespace())
1799 .take(40)
1800 .collect();
1801 if seen_fragments.contains(&fragment) {
1802 continue;
1803 }
1804 seen_fragments.insert(fragment);
1805 final_surprises.push(s);
1806 }
1807 let surprises = final_surprises;
1808 let post_dedup_len = surprises.len();
1809 if pre_dedup_len > post_dedup_len + 1 {
1810 }
1812 let pattern_count = surprises.iter()
1814 .filter(|s| !s.starts_with("rare pattern") && !s.starts_with("unique line"))
1815 .count();
1816 let freq_count = surprises.len() - pattern_count;
1817
1818 let limit = if pattern_count >= 3 {
1822 pattern_count.min(8) } else if pattern_count >= 1 {
1824 (pattern_count + freq_count.min(2)).min(8)
1825 } else {
1826 0
1828 };
1829
1830 surprises.into_iter().take(limit).collect()
1831}
1832
1833fn add_surprise(surprises: &mut Vec<String>, seen: &mut HashSet<String>, s: String) {
1834 let key: String = s.chars().take(60).collect();
1835 if !seen.contains(&key) {
1836 seen.insert(key);
1837 surprises.push(s);
1838 }
1839}
1840
1841fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1844 if pattern == r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{" {
1846 return manual_capture_len_guard(text);
1847 }
1848 if pattern == r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)" {
1849 return manual_capture_range(text);
1850 }
1851 if pattern == r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)" {
1852 return manual_capture_sentinel(text);
1853 }
1854 if pattern == r"\w+\s*>\s*\w+\s*\+\s*(\d+)" {
1855 return manual_capture_gt_plus(text);
1856 }
1857 if pattern == r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+" {
1858 return if text.contains("_threshold") { Some("found".to_string()) } else { None };
1859 }
1860 if pattern == r"let\s+(\w+)\s*=\s*skeleton\(\w+\)" {
1861 return manual_capture_skeleton_let(text);
1862 }
1863 None
1864}
1865
1866fn manual_capture_len_guard(text: &str) -> Option<String> {
1867 let bytes = text.as_bytes();
1868 let mut i = 0;
1869 while i < bytes.len() {
1870 if i + 2 < bytes.len() && bytes[i] == b'i' && bytes[i+1] == b'f' {
1871 i += 2;
1873 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1874 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1876 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; } }
1878 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1880 if i < bytes.len() && bytes[i] == b'<' { i += 1; }
1881 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1882 let start = i;
1884 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1885 if i > start {
1886 return Some(String::from_utf8_lossy(&bytes[start..i]).to_string());
1887 }
1888 }
1889 i += 1;
1890 }
1891 None
1892}
1893
1894fn manual_capture_range(text: &str) -> Option<String> {
1895 let bytes = text.as_bytes();
1896 let mut i = 0;
1897 while i < bytes.len() {
1898 if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i+1] == b'=' {
1899 i += 2;
1900 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1901 let start = i;
1902 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1903 if i > start {
1904 let first = String::from_utf8_lossy(&bytes[start..i]).to_string();
1905 while i < bytes.len() && bytes[i] != b'<' { i += 1; }
1907 if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i+1] == b'=' { i += 2; }
1908 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1909 let start2 = i;
1910 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1911 if i > start2 {
1912 let second = String::from_utf8_lossy(&bytes[start2..i]).to_string();
1913 return Some(format!("{},{}", first, second));
1914 }
1915 }
1916 }
1917 i += 1;
1918 }
1919 None
1920}
1921
1922fn manual_capture_sentinel(text: &str) -> Option<String> {
1923 if let Some(idx) = text.find("is_empty") {
1924 let after = text.get(idx + 9..)?;
1927 if let Some(ret_idx) = after.find("return") {
1928 let after_ret = &after[ret_idx + 6..];
1929 let end = after_ret
1930 .find([';', '}', '\n'])
1931 .unwrap_or(after_ret.len());
1932 return Some(after_ret[..end].trim().to_string());
1933 }
1934 }
1935 None
1936}
1937
1938fn manual_capture_gt_plus(text: &str) -> Option<String> {
1939 if let Some(idx) = text.find('>') {
1940 let after = &text[idx + 1..];
1941 let trimmed = after.trim_start();
1943 let after_ident = trimmed
1945 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1946 let trimmed2 = after_ident.trim_start();
1947 if let Some(rest) = trimmed2.strip_prefix('+') {
1948 let trimmed3 = rest.trim_start();
1949 let end = trimmed3
1950 .find(|c: char| !c.is_ascii_digit())
1951 .unwrap_or(trimmed3.len());
1952 if end > 0 {
1953 return Some(trimmed3[..end].to_string());
1954 }
1955 }
1956 }
1957 None
1958}
1959
1960fn manual_capture_skeleton_let(text: &str) -> Option<String> {
1961 if let Some(idx) = text.find("let ") {
1962 let after = &text[idx + 4..];
1963 let after_ident = after
1964 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1965 let trimmed = after_ident.trim_start();
1966 if let Some(after_eq) = trimmed.strip_prefix('=') {
1967 let after_eq = after_eq.trim_start();
1968 if let Some(sk_idx) = after_eq.find("skeleton(") {
1969 let before = &after_eq[..sk_idx].trim();
1970 if !before.is_empty() {
1971 return Some(before.to_string());
1972 }
1973 }
1974 }
1975 }
1976 None
1977}
1978
1979fn find_magic_numbers(code: &str) -> Vec<String> {
1980 let mut nums = Vec::new();
1981 let bytes = code.as_bytes();
1983 let mut i = 0;
1984 while i < bytes.len() {
1985 if (bytes[i] == b'>' || bytes[i] == b'<' || bytes[i] == b'=' || bytes[i] == b'!')
1986 && i + 1 < bytes.len()
1987 {
1988 i += 1;
1990 if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
1991 i += 1;
1992 }
1993 while i < bytes.len() && bytes[i] == b' ' {
1995 i += 1;
1996 }
1997 let start = i;
1999 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
2000 i += 1;
2001 }
2002 if i > start {
2003 let num = String::from_utf8_lossy(&bytes[start..i]).to_string();
2004 if num != "0" && num != "1" {
2005 nums.push(num);
2006 }
2007 }
2008 } else {
2009 i += 1;
2010 }
2011 }
2012 nums
2013}
2014
2015fn label_number(num: &str, code: &str) -> String {
2016 let lower = code.to_ascii_lowercase();
2017 if lower.contains("uuid") || (num == "8" || num == "13" || num == "18" || num == "23") {
2018 format!("UUID position: {}", num)
2019 } else if lower.contains("hash") || lower.contains("hex") {
2020 format!("hex-hash bound: {}", num)
2021 } else if lower.contains("entropy") || lower.contains("threshold") {
2022 format!("threshold: {}", num)
2023 } else if lower.contains("len") || lower.contains("length") || lower.contains("count") {
2024 format!("length: {}", num)
2025 } else if lower.contains("capacity") {
2026 format!("capacity: {}", num)
2027 } else {
2028 format!("constant: {}", num)
2029 }
2030}
2031
2032fn extract_condition(code: &str) -> Option<String> {
2033 let if_pos = code.find("if ")?;
2034 let after = &code[if_pos + 3..];
2035 let end = after
2037 .find(['{', '\n'])
2038 .unwrap_or(after.len());
2039 Some(after[..end].trim().to_string())
2040}
2041
2042fn extract_return_value(code: &str) -> Option<String> {
2043 let ret_pos = code.find("return")?;
2044 let after = &code[ret_pos + 6..];
2045 let end = after.find([';', '}']).unwrap_or(after.len());
2046 Some(after[..end].trim().to_string())
2047}
2048
2049fn extract_unwrap_default(code: &str) -> Option<String> {
2050 let pos = code.find("unwrap_or(")?;
2051 let after = &code[pos + 10..];
2052 let end = after.find(')')?;
2053 Some(after[..end].trim().to_string())
2054}
2055
2056fn extract_capacity(code: &str) -> Option<String> {
2057 let pos = code.find("with_capacity(")?;
2058 let after = &code[pos + 14..];
2059 let end = after.find(')')?;
2060 Some(after[..end].trim().to_string())
2061}
2062
2063fn build_cross_refs_from_index(
2073 db: &Connection,
2074 symbols: &[Symbol],
2075) -> FxHashMap<String, Vec<String>> {
2076 build_cross_refs_inner(db, symbols).unwrap_or_default()
2078}
2079
2080fn build_cross_refs_inner(
2081 db: &Connection,
2082 symbols: &[Symbol],
2083) -> Result<FxHashMap<String, Vec<String>>, rusqlite::Error> {
2084 let def_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2085 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
2086 let mut ref_set: FxHashMap<String, HashSet<String>> = FxHashMap::default();
2087
2088 let mut all_pairs_stmt = db.prepare(
2093 "SELECT o.file_id, p.phrase, o.is_def
2094 FROM occurrence o
2095 JOIN phrases p ON o.phrase_id = p.id
2096 WHERE length(p.phrase) BETWEEN 3 AND 30
2097 ORDER BY o.file_id
2098 LIMIT 200000",
2099 )?;
2100
2101 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
2103 for row in all_pairs_stmt
2104 .query_map([], |row| {
2105 Ok((
2106 row.get::<_, i64>(0)?,
2107 row.get::<_, String>(1)?,
2108 row.get::<_, i64>(2)? != 0,
2109 ))
2110 })?
2111 .flatten()
2112 {
2113 let (file_id, phrase, is_def) = row;
2114 file_to_phrases
2115 .entry(file_id)
2116 .or_default()
2117 .push((phrase, is_def));
2118 }
2119
2120 let mut phrase_to_non_def_files: FxHashMap<String, Vec<i64>> = FxHashMap::default();
2122 for (file_id, pairs) in &file_to_phrases {
2123 for (phrase, is_def) in pairs {
2124 if !*is_def {
2125 phrase_to_non_def_files
2126 .entry(phrase.clone())
2127 .or_default()
2128 .push(*file_id);
2129 }
2130 }
2131 }
2132
2133 for sym in symbols {
2135 let mut refs: HashSet<String> = HashSet::new();
2136
2137 let files_using_sym = match phrase_to_non_def_files.get(&sym.name) {
2138 Some(f) => f,
2139 None => continue,
2140 };
2141
2142 for &file_id in files_using_sym {
2143 if let Some(pairs) = file_to_phrases.get(&file_id) {
2144 for (phrase, is_def) in pairs {
2145 if *is_def
2146 && phrase != &sym.name
2147 && def_names.contains(phrase.as_str())
2148 && !is_common_word(phrase)
2149 && phrase.len() >= 3
2150 && phrase.len() < 30
2151 && !phrase.starts_with('_')
2152 && !phrase.ends_with("Error")
2153 && !phrase.ends_with("Result")
2154 && !phrase.ends_with("Option")
2155 && phrase != "Self"
2156 {
2157 refs.insert(phrase.clone());
2158 }
2159 }
2160 }
2161
2162 if refs.len() >= 5 {
2164 break;
2165 }
2166 }
2167
2168 if !refs.is_empty() {
2170 let mut sorted: Vec<String> = refs.into_iter().collect();
2171 sorted.sort();
2172 sorted.truncate(3);
2173 ref_set.insert(sym.name.clone(), sorted.into_iter().collect());
2174 }
2175 }
2176
2177 for (name, refs) in ref_set {
2179 let mut sorted: Vec<String> = refs.into_iter().collect();
2180 sorted.sort();
2181 sorted.truncate(3);
2182 cross_refs.insert(name, sorted);
2183 }
2184
2185 Ok(cross_refs)
2186}
2187
2188fn read_symbols_for_subset(
2196 selected: &[Symbol],
2197 _all_symbols: &[Symbol],
2198) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2199 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2200 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2201
2202 for sym in selected {
2206 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2207 meta.lines.to_vec()
2208 } else {
2209 std::fs::read_to_string(&sym.file)
2211 .map(|c| c.lines().map(String::from).collect())
2212 .unwrap_or_default()
2213 };
2214
2215 let start = (sym.line as usize).min(lines.len().saturating_sub(1));
2216 let end = find_body_end(lines.as_slice(), start, sym.indent).min(lines.len());
2217 if start < end {
2218 for line in &lines[start..end] {
2220 let trimmed = line.trim();
2221 if trimmed.is_empty() {
2222 continue;
2223 }
2224 let skel = aggressive_skeleton(trimmed);
2225 if !skel.is_empty() {
2226 *freq.entry(skel).or_insert(0) += 1;
2227 }
2228 }
2229 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2230 }
2231 }
2232
2233 (sources, freq)
2234}
2235
2236fn read_symbols_and_frequency(
2239 symbols: &[Symbol],
2240) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2241 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2242 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2243
2244 for sym in symbols {
2247 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2248 meta.lines.to_vec()
2249 } else {
2250 std::fs::read_to_string(&sym.file)
2251 .map(|c| c.lines().map(String::from).collect())
2252 .unwrap_or_default()
2253 };
2254
2255 let start = (sym.line.max(0) as usize).min(lines.len().saturating_sub(1));
2260 let brace_end = find_body_end(lines.as_slice(), start, sym.indent);
2261 let index_end = (sym.end_line as usize).max(start + 1).min(lines.len());
2262 let end = brace_end.max(index_end).min(lines.len());
2263
2264 if start < end {
2265 for line in &lines[start..end] {
2267 let trimmed = line.trim();
2268 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
2269 continue;
2270 }
2271 let skel = aggressive_skeleton(trimmed);
2272 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 5 {
2273 continue;
2274 }
2275 *freq.entry(skel).or_insert(0) += 1;
2276 }
2277
2278 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2279 }
2280 }
2281
2282 (sources, freq)
2283}
2284
2285fn find_body_end(lines: &[String], start: usize, _base_indent: i32) -> usize {
2290 if start >= lines.len() {
2291 return start;
2292 }
2293 let first_line = &lines[start];
2294
2295 let stripped = first_line.trim();
2297 if stripped.starts_with("def ")
2298 || stripped.starts_with("class ")
2299 || stripped.starts_with("async def ")
2300 {
2301 let first_indent = first_line.len() - first_line.trim_start().len();
2302 for (i, line) in lines.iter().enumerate().skip(start + 1) {
2303 let trimmed = line.trim();
2304 if trimmed.is_empty() || trimmed.starts_with("#") {
2305 continue;
2306 }
2307 let indent = line.len() - line.trim_start().len();
2308 if indent <= first_indent
2310 && (trimmed.starts_with("def ")
2311 || trimmed.starts_with("class ")
2312 || trimmed.starts_with("async def "))
2313 {
2314 return i;
2315 }
2316 }
2317 return lines.len();
2318 }
2319
2320 let mut depth: i32 = 0;
2322 let mut seen_open = false;
2323 for (i, line) in lines.iter().enumerate().skip(start) {
2324 let trimmed = line.trim();
2325
2326 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
2328 continue;
2329 }
2330
2331 for ch in line.chars() {
2332 match ch {
2333 '{' => { depth += 1; seen_open = true; }
2334 '}' => { depth -= 1; }
2335 _ => {}
2336 }
2337 }
2338
2339 if seen_open && depth == 0 {
2340 return i + 1;
2341 }
2342 }
2343 lines.len()
2344}
2345
2346fn derive_crate_name(file_path: &str) -> String {
2353 let parts: Vec<&str> = file_path.split('/').collect();
2354 for (i, part) in parts.iter().enumerate() {
2355 if (*part == "crates" || *part == "packages") && i + 1 < parts.len() {
2356 return parts[i + 1].to_string();
2357 }
2358 }
2359 if parts.len() >= 2 {
2364 let parent = parts[parts.len() - 2];
2365 const GENERIC: &[&str] = &[
2366 "src", "lib", "app", "apps", "source", "sources", "test", "tests",
2367 "spec", "specs", "bin", "cmd", "main", "code", "internal", "pkg",
2368 ];
2369 if !GENERIC.contains(&parent) {
2370 return parent.to_string();
2371 }
2372 }
2373 String::new()
2374}
2375
2376fn detect_modules(symbols: &[Symbol]) -> Vec<(String, Vec<Symbol>)> {
2377 let mut module_map: FxHashMap<String, Vec<Symbol>> = FxHashMap::default();
2378
2379 for sym in symbols {
2380 let module = derive_module_name(&sym.file);
2381 module_map.entry(module).or_default().push(sym.clone());
2382 }
2383
2384 let mut modules: Vec<(String, Vec<Symbol>)> = module_map.into_iter().collect();
2385 modules.sort_by(|a, b| a.0.cmp(&b.0));
2386 modules
2387}
2388
2389fn derive_module_name(file_path: &str) -> String {
2390 let path = Path::new(file_path);
2391 let components: Vec<_> = path.components().collect();
2392
2393 for (i, comp) in components.iter().enumerate() {
2395 let part = comp.as_os_str().to_string_lossy().to_string();
2396 if part == "packages" && i + 1 < components.len() {
2397 return format!("packages/{}", components[i + 1].as_os_str().to_string_lossy());
2398 }
2399 if part == "src" && i + 1 < components.len() {
2400 return format!("src/{}", components[i + 1].as_os_str().to_string_lossy());
2401 }
2402 if part == "crates" && i + 1 < components.len() {
2403 return format!("crates/{}", components[i + 1].as_os_str().to_string_lossy());
2404 }
2405 }
2406
2407 components
2409 .first()
2410 .map(|c| c.as_os_str().to_string_lossy().to_string())
2411 .unwrap_or_else(|| "root".to_string())
2412}
2413
2414fn build_cochange_line(_name: &str, signature: &str, _body: &str, cross_refs: &[String]) -> String {
2420 let lower = signature.to_ascii_lowercase();
2421 let mut parts: Vec<String> = Vec::new();
2422 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2423 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2424
2425 if is_enum {
2426 if !cross_refs.is_empty() {
2427 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2428 parts.push(format!("Add variant → update match sites: {}", sites.join(", ")));
2429 }
2430 } else if is_struct {
2431 if !cross_refs.is_empty() {
2432 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2433 parts.push(format!("Add field → update constructors: {}", sites.join(", ")));
2434 }
2435 } else {
2436 if !cross_refs.is_empty() {
2437 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2438 parts.push(format!("Rename → update {} caller(s): {}", cross_refs.len(), sites.join(", ")));
2439 }
2440 }
2441 parts.join("; ")
2442}
2443
2444fn expand_type_signature(signature: &str, body: &str, def_line: i32) -> String {
2445 let sig_trimmed = signature.trim();
2446 let lower = sig_trimmed.to_ascii_lowercase();
2447 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2448 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2449 if !is_enum && !is_struct {
2450 return signature.to_string();
2451 }
2452 let mut names: Vec<String> = Vec::new();
2453 for (j, line) in body.lines().enumerate().skip(1) {
2454 let trimmed = line.trim();
2455 if trimmed == "}" || trimmed.is_empty() { continue; }
2456 if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with('*') {
2459 continue;
2460 }
2461 let abs_line = def_line + 1 + j as i32;
2465 if is_enum {
2466 let name = trimmed.split([',', '{', '(', ':']).next().unwrap_or("").trim().trim_end_matches(',');
2467 if !name.is_empty() && name.len() > 1 && name.chars().next().is_some_and(|c| c.is_uppercase() || c == '_') {
2468 names.push(format!("{} (line {})", name, abs_line));
2469 }
2470 } else if is_struct {
2471 if let Some(colon_pos) = trimmed.find(':') {
2472 let name = trimmed[..colon_pos].trim().trim_start_matches("pub ").trim();
2473 let ftype = trimmed[colon_pos + 1..]
2474 .trim()
2475 .trim_end_matches(',')
2476 .trim()
2477 .to_string();
2478 if !name.is_empty() && name.len() > 1 {
2479 if ftype.is_empty() || ftype == "{" {
2482 names.push(format!("{} (line {})", name, abs_line));
2483 } else {
2484 let ft = ftype.split('{').next().unwrap_or(&ftype).trim();
2485 names.push(format!("{}: {} (line {})", name, ft, abs_line));
2486 }
2487 }
2488 }
2489 }
2490 }
2491 if names.is_empty() { return signature.to_string(); }
2492 let kind = if is_enum { "variants" } else { "fields" };
2493 format!("{} // {}({})", sig_trimmed, kind, names.join(", "))
2494}
2495
2496fn render_entry(
2497 sym: &Symbol,
2498 body: &str,
2499 cross_refs: &[String],
2500 skeleton_freq: &FxHashMap<String, usize>,
2501 format: PackFormat,
2502) -> String {
2503 let crate_name = derive_crate_name(&sym.file);
2505 let header = if crate_name.is_empty() {
2506 format!("## {}", sym.name)
2507 } else {
2508 format!("## {}/{}", sym.name, crate_name)
2509 };
2510 let mut parts = vec![header];
2511
2512 if matches!(format, PackFormat::Full) && !sym.doc_comment.is_empty() {
2514 let l0 = sym
2515 .doc_comment
2516 .split('.')
2517 .next()
2518 .unwrap_or(&sym.doc_comment)
2519 .trim();
2520 let l0 = if l0.chars().count() > 120 {
2521 l0.chars().take(120).collect::<String>()
2523 } else {
2524 l0.to_string()
2525 };
2526 parts.push(format!("L0: {}", l0));
2527 }
2528
2529 let l2_line = expand_type_signature(&sym.signature, body, sym.line);
2531 parts.push(format!("L2: {} [{}:{}]", l2_line, sym.file, sym.line + 1));
2534
2535 if !body.is_empty() {
2537 let surprises = extract_surprise_from_body(body, skeleton_freq);
2538 if !surprises.is_empty() {
2539 parts.push(format!("L3: {}", surprises.join("; ")));
2540 }
2541 }
2542
2543 if matches!(format, PackFormat::Full) && cross_refs.len() >= 2 {
2545 parts.push(format!("Cross-refs: {}", cross_refs.iter().take(5).cloned().collect::<Vec<_>>().join(", ")));
2546 }
2547
2548 let l4 = build_cochange_line(&sym.name, &sym.signature, body, cross_refs);
2550 if !l4.is_empty() {
2551 parts.push(format!("L4: {}", l4));
2552 }
2553
2554 parts.push(String::new());
2555 parts.join("\n")
2556}
2557
2558#[cfg(test)]
2559mod noise_tests {
2560 use super::is_noise_line;
2561
2562 #[test]
2565 fn noise_classes_are_filtered() {
2566 let noise = [
2567 "",
2569 "}",
2570 "});",
2571 "};",
2572 "];",
2573 ")",
2574 "self.assertEqual(request.status, 200)",
2576 "self.assertIsNotNone(request.body)",
2577 "self.assertTrue(request.valid)",
2578 "block_phrases.append(request.text)",
2580 "results.append(item)",
2581 "log::info!(\"starting engine\")",
2583 "tracing::debug!(\"rpm = {}\", self.rpm)",
2584 "println!(\"done\")",
2585 "dbg!(value);",
2586 "#[derive(Debug, Clone, PartialEq)]",
2588 "derive(Serialize, Deserialize)",
2589 "break;",
2591 "continue;",
2592 "return None;",
2594 "return true;",
2595 "return false;",
2596 "let _ = do_thing();",
2598 "value.ok()?;",
2599 "thing.expect(\"must exist\");",
2600 "let x = y;",
2602 "let mut count = 0;",
2603 ];
2604 for line in noise {
2605 assert!(
2606 is_noise_line(line),
2607 "expected to be filtered as noise: {:?}",
2608 line
2609 );
2610 }
2611 }
2612
2613 #[test]
2615 fn signal_lines_are_preserved() {
2616 let signal = [
2617 "hash = hash.wrapping_mul(33).wrapping_add(byte);",
2618 "if total > threshold && !exhausted {",
2619 "match response_kind {",
2620 "let ratio = scored(matched, total) as f64;",
2621 "return Some(compute_fallback(input));",
2622 "buffer.extend_from_slice(&payload);",
2623 "channel.send(Message::Shutdown).await;",
2624 ];
2625 for line in signal {
2626 assert!(
2627 !is_noise_line(line),
2628 "expected to be preserved as signal: {:?}",
2629 line
2630 );
2631 }
2632 }
2633}
2634
2635#[cfg(test)]
2636mod v80_panic_tests {
2637 use super::*;
2638 #[test]
2639 fn manual_capture_sentinel_no_panic() {
2640 for t in ["is_empty", "x is_empty", "// is_empty", "if self.is_empty", "is_empty\nreturn x;"] {
2642 let _ = manual_capture_sentinel(t); }
2644 }
2645}