1use std::collections::HashSet;
32use rustc_hash::FxHashMap;
33use std::path::Path;
34
35use rusqlite::Connection;
36
37use reliary_sift::classify::aggressive_skeleton;
38
39#[derive(Debug, Clone, Copy)]
45pub enum PackFormat {
46 L2L3,
48 Full,
50 BodySnippets,
53}
54
55#[derive(Debug, Clone, Copy)]
62pub enum GateDecision {
63 Full,
65 Minimal,
67 Skip,
69}
70
71pub fn compute_complexity_score(path: &str) -> Result<f32, String> {
79 let db_path = format!(
80 "{}/.reliary/index.sqlite",
81 path.trim_end_matches('/')
82 );
83 let db = Connection::open(&db_path)
84 .map_err(|e| format!("cannot open index at {}: {}", db_path, e))?;
85 reliary_search::schema::open_existing_db_safe(&db)
86 .map_err(|e| format!("index not initialized: {}", e))?;
87 compute_complexity_score_for(&db, path)
89}
90
91pub fn gate_decision(score: f32) -> GateDecision {
105 const HIGH_THRESHOLD: f32 = 6.0;
106 const LOW_THRESHOLD: f32 = 4.0;
107
108 if score > HIGH_THRESHOLD {
109 GateDecision::Full
110 } else if score > LOW_THRESHOLD {
111 GateDecision::Minimal
112 } else {
113 GateDecision::Skip
114 }
115}
116
117pub fn should_inject_pack(path: &str) -> Result<GateDecision, String> {
119 let score = compute_complexity_score(path)?;
120 Ok(gate_decision(score))
121}
122
123pub fn gate_decision_for(conn: &Connection, path: &str) -> Result<GateDecision, String> {
125 let score = compute_complexity_score_for(conn, path)?;
126 Ok(gate_decision(score))
127}
128
129pub fn compute_complexity_score_for(conn: &Connection, _path: &str) -> Result<f32, String> {
132 let row: (i64, i64) = conn.query_row(
135 "SELECT
136 COUNT(DISTINCT CASE WHEN o.is_def = 1 AND (
137 f.file_path LIKE '%.rs' OR f.file_path LIKE '%.py' OR f.file_path LIKE '%.ts'
138 OR f.file_path LIKE '%.go' OR f.file_path LIKE '%.c' OR f.file_path LIKE '%.js'
139 OR f.file_path LIKE '%.java' OR f.file_path LIKE '%.rb' OR f.file_path LIKE '%.swift'
140 OR f.file_path LIKE '%.kt' OR f.file_path LIKE '%.zig' OR f.file_path LIKE '%.nim'
141 ) THEN p.phrase END),
142 COUNT(CASE WHEN o.is_def = 0 AND (
143 f.file_path NOT LIKE '%/bench/%' AND f.file_path NOT LIKE '%/tests/%'
144 AND f.file_path NOT LIKE '%/test/%' AND f.file_path NOT LIKE '%/target/%'
145 AND f.file_path NOT LIKE '%/vendor/%' AND f.file_path NOT LIKE '%/node_modules/%'
146 AND f.file_path NOT LIKE '%/__pycache__/%' AND f.file_path NOT LIKE '%/.specstory/%'
147 ) THEN 1 END)
148 FROM occurrence o
149 JOIN phrases p ON o.phrase_id = p.id
150 JOIN file_map f ON o.file_id = f.id",
151 [], |r| Ok((r.get(0)?, r.get(1)?)),
152 ).unwrap_or((0, 0));
153
154 let (symbol_count, non_def_count) = row;
155 if symbol_count == 0 {
156 return Ok(0.0);
157 }
158 let avg_block_span: f32 = conn.query_row(
160 "SELECT AVG(CAST(end_line - start_line AS REAL))
161 FROM block WHERE end_line > start_line",
162 [], |r| r.get::<_, Option<f64>>(0),
163 ).ok().flatten().unwrap_or(0.0) as f32;
164
165 let specific_count: i64 = conn.query_row(
166 "SELECT COUNT(DISTINCT p.phrase)
167 FROM phrases p
168 WHERE length(p.phrase) > 8
169 AND (p.phrase LIKE '%\\_%' ESCAPE '\\' OR substr(p.phrase, 2, 1) GLOB '[a-z]'
170 AND substr(p.phrase, 1, 1) GLOB '[A-Z]')",
171 [], |r| r.get(0),
172 ).unwrap_or(0);
173 let total_phrases: i64 = conn.query_row("SELECT COUNT(*) FROM phrases", [], |r| r.get(0)).unwrap_or(1);
174 let specificity_ratio = if total_phrases > 0 { specific_count as f32 / total_phrases as f32 } else { 0.0 };
175
176 let log_symbols = (symbol_count.max(1) as f32).ln();
177 let log_xrefs = (non_def_count.max(1) as f32).ln();
178 let span_score = avg_block_span / 10.0;
179
180 Ok(0.25 * log_symbols + 0.20 * span_score + 0.25 * log_xrefs + 0.30 * specificity_ratio)
181}
182
183pub fn generate_pack(path: &str, format: PackFormat) -> Result<String, String> {
188 let db_path = format!(
189 "{}/.reliary/index.sqlite",
190 path.trim_end_matches('/')
191 );
192 let db = Connection::open(&db_path)
193 .map_err(|e| format!("cannot open index at {}: {}", db_path, e))?;
194
195 reliary_search::schema::open_existing_db_safe(&db)
197 .map_err(|e| format!("index not initialized: {}", e))?;
198
199 let symbols = extract_symbols_from_index(&db)?;
201
202 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
204
205 let cross_refs = build_cross_refs_from_index(&db, &symbols);
207
208 let mut entries = Vec::new();
210 entries.push(format!(
211 "# Holographic Pack — {} ({} symbols)\n",
212 Path::new(path)
213 .file_name()
214 .map(|n| n.to_string_lossy().to_string())
215 .unwrap_or_else(|| path.to_string()),
216 symbols.len()
217 ));
218
219 for sym in &symbols {
220 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
221 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
222 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
223 entries.push(entry);
224 }
225
226 Ok(entries.join("\n"))
227}
228
229pub fn generate_pack_hotspot(
237 path: &str,
238 format: PackFormat,
239 top_k: usize,
240) -> Result<String, String> {
241 let db = open_index(path)?;
242 generate_pack_hotspot_with(&db, path, format, top_k)
243}
244
245pub fn generate_pack_hotspot_with(
247 db: &Connection,
248 path: &str,
249 format: PackFormat,
250 top_k: usize,
251) -> Result<String, String> {
252 let symbols = extract_symbols_from_index(db)?;
253
254 let inbound_counts = compute_inbound_ref_counts(db, &symbols);
257 let mut scored: Vec<(f32, &Symbol)> = symbols
258 .iter()
259 .map(|s| {
260 let inbound = *inbound_counts.get(&s.name).unwrap_or(&0) as f32;
261 (0.6 * inbound, s) })
263 .collect();
264 let k = top_k.min(scored.len());
266 if k < scored.len() {
267 scored.select_nth_unstable_by(k, |a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
268 }
269 scored.truncate(k);
270
271 let selected: Vec<Symbol> = scored
272 .into_iter()
273 .map(|(_, s)| s.clone())
274 .collect();
275
276 let (symbol_sources, skeleton_freq) = read_symbols_for_subset(&selected, &symbols);
277
278 let cross_refs = build_cross_refs_for_subset(db, &selected, &symbols);
280
281 let mut entries = Vec::with_capacity(selected.len() + 1);
282 entries.push(format!(
283 "# Holographic Pack — {} ({} of {} symbols, hotspot-ranked)\n",
284 Path::new(path)
285 .file_name()
286 .map(|n| n.to_string_lossy().to_string())
287 .unwrap_or_else(|| path.to_string()),
288 selected.len(),
289 symbols.len()
290 ));
291
292 for sym in &selected {
293 let refs = cross_refs.get(&sym.name).map(|v| v.as_slice()).unwrap_or(&[]);
295 let source = symbol_sources.get(&sym.name).map(|s| s.as_str()).unwrap_or("");
296 let entry = render_entry(sym, source, refs, &skeleton_freq, format);
297 entries.push(entry);
298 }
299
300 let result = entries.join("\n");
301 Ok(result)
302}
303
304fn compute_inbound_ref_counts(
308 db: &Connection,
309 symbols: &[Symbol],
310) -> FxHashMap<String, usize> {
311 let mut counts: FxHashMap<String, usize> = FxHashMap::default();
312 let symbol_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
313
314 let mut stmt = match db.prepare(
317 "SELECT p.phrase, COUNT(*) as cnt
318 FROM occurrence o
319 JOIN phrases p ON o.phrase_id = p.id
320 JOIN file_map f ON o.file_id = f.id
321 WHERE o.is_def = 0
322 AND f.file_path NOT LIKE '%/bench/%'
323 AND f.file_path NOT LIKE '%/tests/%'
324 AND f.file_path NOT LIKE '%/test/%'
325 AND f.file_path NOT LIKE '%/target/%'
326 AND f.file_path NOT LIKE '%/vendor/%'
327 AND f.file_path NOT LIKE '%/node_modules/%'
328 AND f.file_path NOT LIKE '%/__pycache__/%'
329 AND f.file_path NOT LIKE '%/.specstory/%'
330 GROUP BY p.phrase",
331 ) {
332 Ok(s) => s,
333 Err(_) => return counts,
334 };
335
336 let rows = stmt.query_map([], |row| {
337 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
338 });
339
340 if let Ok(rows) = rows {
341 for row in rows.flatten() {
342 let (phrase, cnt) = row;
343 if symbol_names.contains(phrase.as_str()) {
345 counts.insert(phrase, cnt as usize);
346 }
347 }
348 }
349
350 counts
351}
352
353fn build_cross_refs_for_subset(
356 db: &Connection,
357 selected: &[Symbol],
358 all_symbols: &[Symbol],
359) -> FxHashMap<String, Vec<String>> {
360 let all_names: HashSet<&str> = all_symbols.iter().map(|s| s.name.as_str()).collect();
361 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
362
363 let mut all_pairs_stmt = match db.prepare(
370 "SELECT o.file_id, p.phrase, o.is_def
371 FROM occurrence o
372 JOIN phrases p ON o.phrase_id = p.id
373 WHERE p.phrase IN (SELECT phrase FROM phrases WHERE length(phrase) BETWEEN 3 AND 30)
374 ORDER BY o.file_id LIMIT 200000",
375 ) {
376 Ok(s) => s,
377 Err(_) => return cross_refs,
378 };
379
380 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
381 for row in all_pairs_stmt
382 .query_map([], |row| {
383 Ok((
384 row.get::<_, i64>(0)?,
385 row.get::<_, String>(1)?,
386 row.get::<_, i64>(2)? != 0,
387 ))
388 })
389 .ok()
390 .into_iter()
391 .flatten()
392 .filter_map(|r| r.ok())
393 {
394 let (file_id, phrase, is_def) = row;
395 file_to_phrases.entry(file_id).or_default().push((phrase, is_def));
396 }
397
398 for sym in selected {
401 let usage_files: Vec<i64> = file_to_phrases
403 .iter()
404 .filter(|(_, pairs)| pairs.iter().any(|(p, is_def)| *p == sym.name && !*is_def))
405 .map(|(fid, _)| *fid)
406 .take(50)
407 .collect();
408
409 let mut refs: HashSet<String> = HashSet::new();
410 for file_id in usage_files {
411 if let Some(pairs) = file_to_phrases.get(&file_id) {
412 for (phrase, is_def) in pairs {
413 if *is_def
414 && *phrase != sym.name
415 && all_names.contains(phrase.as_str())
416 && !is_common_word(phrase)
417 {
418 refs.insert(phrase.clone());
419 }
420 if refs.len() >= 6 {
421 break;
422 }
423 }
424 }
425 if refs.len() >= 6 {
426 break;
427 }
428 }
429
430 cross_refs.insert(sym.name.clone(), refs.into_iter().take(6).collect());
431 }
432
433 cross_refs
434}
435
436pub fn generate_pack_auto(path: &str) -> Result<String, String> {
442 let conn = open_index(path)?;
444 let decision = gate_decision_for(&conn, path)?;
445
446 match decision {
447 GateDecision::Skip => Ok(String::new()),
448 GateDecision::Minimal => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 15),
449 GateDecision::Full => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 50),
450 }
451}
452
453fn open_index(codebase_path: &str) -> Result<rusqlite::Connection, String> {
455 let db_path = format!(
456 "{}/.reliary/index.sqlite",
457 codebase_path.trim_end_matches('/')
458 );
459 let conn = Connection::open(&db_path)
460 .map_err(|e| format!("pack: open {}: {}", db_path, e))?;
461 reliary_search::schema::open_existing_db_safe(&conn)
462 .map_err(|e| format!("index not initialized: {}", e))?;
463 Ok(conn)
464}
465
466#[derive(Debug, Clone)]
468pub struct PackEntry {
469 pub name: String,
470 pub header: String, pub body: String, }
473
474pub fn parse_pack_entries(pack: &str) -> Vec<PackEntry> {
476 let mut entries = Vec::new();
477 let mut current_name: Option<String> = None;
478 let mut current_header: String = String::new();
479 let mut current_body: String = String::new();
480
481 for line in pack.lines() {
482 if line.starts_with("## ") {
483 if let Some(name) = current_name.take() {
485 entries.push(PackEntry {
486 name,
487 header: std::mem::take(&mut current_header),
488 body: std::mem::take(&mut current_body),
489 });
490 }
491 current_name = Some(line.strip_prefix("///").unwrap_or(line).split('/').next().unwrap_or("").trim().to_string());
492 current_header = line.to_string();
493 } else if current_name.is_some() {
494 if !current_body.is_empty() {
495 current_body.push('\n');
496 }
497 current_body.push_str(line);
498 }
499 }
500 if let Some(name) = current_name {
502 entries.push(PackEntry {
503 name,
504 header: current_header,
505 body: current_body,
506 });
507 }
508 entries
509}
510
511fn tokenize(text: &str) -> Vec<String> {
515 reliary_search::tokenize(text)
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum SliceDecision {
533 Slice,
535 Skip,
537}
538
539pub fn should_slice_for_query(query: &str) -> SliceDecision {
543 let text = query.to_ascii_lowercase();
544
545 if text.contains("bug") || text.contains("issue") || text.contains("wrong")
547 || text.contains("incorrect") || text.contains("missing") {
548 return SliceDecision::Slice;
549 }
550 if text.contains("call") || text.contains("caller") || text.contains("who uses")
551 || text.contains("references") || text.contains("called by") {
552 return SliceDecision::Slice;
553 }
554
555 if text.contains("value") || text.contains("threshold") || text.contains("parameter")
557 || text.contains("default") || text.contains("exactly") || text.contains("what number") {
558 return SliceDecision::Slice;
559 }
560
561 let func_indicators = [
565 "find_clusters", "find_clusters_global", "skelhash", "skeleton_hash",
566 "aggressive_skeleton_hash", "maxwell", "maxwellgate", "should_drop",
567 "detect_strategy", "detect_tabular", "detect_json", "callgraph",
568 "is_definition_line", "reliary_sift", "reliary-output",
569 ];
570 for indicator in &func_indicators {
571 if text.contains(indicator) {
572 return SliceDecision::Slice;
573 }
574 }
575
576 SliceDecision::Skip
579}
580
581pub fn adaptive_slice(pack: &str, query: &str, top_k: usize) -> (String, SliceDecision) {
584 let decision = should_slice_for_query(query);
585 match decision {
586 SliceDecision::Slice => (slice_pack_for_query(pack, query, top_k), decision),
587 SliceDecision::Skip => (String::new(), decision),
588 }
589}
590
591pub fn slice_pack_for_query(pack: &str, query: &str, top_k: usize) -> String {
595 let entries = parse_pack_entries(pack);
596 if entries.is_empty() {
597 return String::new();
598 }
599
600 let query_tokens = tokenize(query);
601 if query_tokens.is_empty() {
602 return entries
604 .into_iter()
605 .take(top_k)
606 .map(|e| format!("{}\n{}", e.header, e.body))
607 .collect::<Vec<_>>()
608 .join("\n\n");
609 }
610
611 let n_docs = entries.len() as f64;
613 let mut doc_tokens: Vec<Vec<String>> = Vec::with_capacity(entries.len());
614 for entry in &entries {
615 let combined = format!("{} {}", entry.name, entry.body);
617 doc_tokens.push(tokenize(&combined));
618 }
619
620 let mut df: FxHashMap<String, usize> = FxHashMap::default();
622 for tokens in &doc_tokens {
623 let unique: HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
624 for t in &query_tokens {
625 if unique.contains(t.as_str()) {
626 *df.entry(t.clone()).or_insert(0) += 1;
627 }
628 }
629 }
630
631 let total_len: usize = doc_tokens.iter().map(|d| d.len()).sum();
633 let avgdl = if n_docs > 0.0 {
634 total_len as f64 / n_docs
635 } else {
636 1.0
637 };
638
639 let mut scored: Vec<(f64, &PackEntry)> = Vec::with_capacity(entries.len());
641 for (i, entry) in entries.iter().enumerate() {
642 let doc_len = doc_tokens[i].len() as f64;
643 let mut score = 0.0;
644 let mut tf: FxHashMap<&str, usize> = FxHashMap::default();
646 for t in &doc_tokens[i] {
647 *tf.entry(t.as_str()).or_insert(0) += 1;
648 }
649 for qt in &query_tokens {
650 let d_freq = *df.get(qt).unwrap_or(&0) as f64;
651 if d_freq == 0.0 {
652 continue;
653 }
654 let idf = ((n_docs - d_freq + 0.5) / (d_freq + 0.5) + 1.0).ln();
655 let term_freq = *tf.get(qt.as_str()).unwrap_or(&0) as f64;
656 if term_freq > 0.0 {
657 let bm25 = idf * (term_freq * (1.2 + 1.0))
658 / (term_freq + 1.2 * (1.0 - 0.75 + 0.75 * (doc_len / avgdl)));
659 score += bm25;
660 }
661 }
662 let name_lower = entry.name.to_ascii_lowercase();
664 for qt in &query_tokens {
665 if name_lower == *qt {
666 score += 5.0; } else if name_lower.contains(qt.as_str()) {
668 score += 2.0; }
670 }
671 scored.push((score, entry));
672 }
673
674 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
676
677 let selected: Vec<&PackEntry> = scored
679 .into_iter()
680 .filter(|(s, _)| *s > 0.0)
681 .take(top_k)
682 .map(|(_, e)| e)
683 .collect();
684
685 if selected.is_empty() {
686 return String::new();
687 }
688
689 let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
694 let mut entries_by_name: FxHashMap<&str, &PackEntry> = FxHashMap::default();
696 for e in &entries {
697 entries_by_name.entry(e.name.as_str()).or_insert(e);
698 }
699 let mut expanded: Vec<&PackEntry> = selected.clone();
700 let mut expanded_names: HashSet<String> = selected_names.clone();
701 let mut expansion_count = 0;
702 let max_expansion = 15usize.saturating_sub(top_k.min(15));
703 for entry in &selected {
704 for line in entry.body.lines() {
705 let refs_text = if let Some(rest) = line.strip_prefix("Cross-refs:") {
706 Some(rest.trim())
707 } else if line.starts_with("L4:") && line.contains("caller") {
708 if let Some(pos) = line.find(": ") {
710 let after = &line[pos + 2..];
711 if let Some(last_colon) = after.rfind(": ") {
713 Some(&after[last_colon + 2..])
714 } else {
715 Some(after)
716 }
717 } else {
718 None
719 }
720 } else {
721 None
722 };
723 if let Some(refs) = refs_text {
724 for ref_name in refs.split(',').take(3) {
725 let ref_name = ref_name.trim();
726 if ref_name.is_empty() { continue; }
727 let ref_name_slash = format!("{}/", ref_name);
728 if let Some(e) = entries_by_name.get(ref_name).or_else(|| {
730 entries_by_name.values().find(|e| e.name.starts_with(&ref_name_slash))
731 }) {
732 if !selected_names.contains(&e.name) && !expanded_names.contains(&e.name) {
733 expanded_names.insert(e.name.clone());
734 expanded.push(e);
735 expansion_count += 1;
736 if expansion_count >= max_expansion { break; }
737 }
738 }
739 if expansion_count >= max_expansion { break; }
740 }
741 break; }
743 }
744 if expansion_count >= max_expansion { break; }
745 }
746
747 let total_entries = expanded.len();
748
749 let mut result = format!(
750 "# Sliced Pack — {} entries for query: \"{}\"\n\n",
751 total_entries,
752 query.chars().take(80).collect::<String>()
753 );
754 for entry in &expanded {
755 result.push_str(&format!("{}\n{}\n\n", entry.header, entry.body));
756 }
757 result
758}
759
760pub fn generate_hierarchical_pack(
762 path: &str,
763 format: PackFormat,
764 max_symbols_per_module: usize,
765) -> Result<HierarchicalPack, String> {
766 let db_path = format!(
767 "{}/.reliary/index.sqlite",
768 path.trim_end_matches('/')
769 );
770 let db = Connection::open(&db_path)
771 .map_err(|e| format!("cannot open index: {}", e))?;
772 reliary_search::schema::open_existing_db_safe(&db)
773 .map_err(|e| format!("index not initialized: {}", e))?;
774
775 let symbols = extract_symbols_from_index(&db)?;
776 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
777 let cross_refs = build_cross_refs_from_index(&db, &symbols);
778
779 let modules = detect_modules(&symbols);
781
782 let mut top_parts = vec![format!(
784 "# Holographic Pack — {} (top-level map)\n",
785 Path::new(path)
786 .file_name()
787 .map(|n| n.to_string_lossy().to_string())
788 .unwrap_or_else(|| path.to_string()),
789 )];
790
791 let mut module_packs: Vec<(String, String)> = Vec::new();
792
793 for (module_name, module_syms) in &modules {
794 let sym_count = module_syms.len();
795 let key_syms: Vec<String> = module_syms
796 .iter()
797 .filter(|s| !s.doc_comment.is_empty())
798 .take(5)
799 .map(|s| s.name.clone())
800 .collect();
801
802 top_parts.push(format!("## {}", module_name));
803 top_parts.push(format!("L0: {} symbols across this module", sym_count));
804 if !key_syms.is_empty() {
805 top_parts.push(format!("Key symbols: {}", key_syms.join(", ")));
806 }
807 top_parts.push(String::new());
808
809 let limited: Vec<&Symbol> = module_syms.iter().take(max_symbols_per_module).collect();
811 if limited.is_empty() {
812 continue;
813 }
814 let mut pack_parts = vec![format!("# Holographic Pack — {}\n", module_name)];
815 for sym in limited {
816 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
817 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
818 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
819 pack_parts.push(entry);
820 }
821 module_packs.push((module_name.clone(), pack_parts.join("\n")));
822 }
823
824 Ok(HierarchicalPack {
825 top_level: top_parts.join("\n"),
826 module_packs,
827 })
828}
829
830pub struct HierarchicalPack {
831 pub top_level: String,
832 pub module_packs: Vec<(String, String)>,
833}
834
835#[derive(Debug, Clone)]
840pub struct Symbol {
841 pub name: String,
842 pub signature: String,
843 pub file: String,
844 pub line: i32,
845 pub doc_comment: String,
846 pub block_id: i64,
847 pub end_line: i32,
848 pub indent: i32,
849}
850
851fn extract_symbols_from_index(db: &Connection) -> Result<Vec<Symbol>, String> {
856 let mut stmt = db
857 .prepare(
858 "SELECT o.phrase_id, o.file_id, o.line, o.block_id,
859 p.phrase, f.file_path,
860 b.end_line, b.indent
861 FROM occurrence o
862 JOIN phrases p ON o.phrase_id = p.id
863 JOIN file_map f ON o.file_id = f.id
864 JOIN block b ON o.block_id = b.block_id
865 WHERE o.is_def = 1
866 ORDER BY f.file_path, o.line",
867 )
868 .map_err(|e| format!("query symbols: {}", e))?;
869
870 let rows = stmt
871 .query_map([], |row| {
872 let phrase: String = row.get(4)?;
873 let file_path: String = row.get(5)?;
874 let line: i32 = row.get(2)?;
875 let block_id: i64 = row.get(3)?;
876 let end_line: i32 = row.get(6)?;
877 let indent: i32 = row.get(7)?;
878 Ok((phrase, file_path, line, block_id, end_line, indent))
879 })
880 .map_err(|e| format!("query symbols: {}", e))?;
881
882 let mut symbols: Vec<Symbol> = Vec::new();
883 let mut seen: HashSet<(String, String)> = HashSet::new();
887
888 let mut lines_cache: FxHashMap<String, Vec<String>> = FxHashMap::default();
891
892 for row in rows {
893 let (_phrase, file_path, line, block_id, end_line, indent) =
894 row.map_err(|e| format!("row: {}", e))?;
895
896 if !is_source_file(&file_path) {
898 continue;
899 }
900
901 if !lines_cache.contains_key(&file_path) {
903 let lines_vec: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&file_path) {
904 meta.lines.clone()
905 } else {
906 std::fs::read_to_string(&file_path)
907 .map(|c| c.lines().map(String::from).collect())
908 .unwrap_or_default()
909 };
910 lines_cache.insert(file_path.clone(), lines_vec);
911 }
912
913 let lines = &lines_cache[&file_path];
914
915 let signature = read_signature_line(lines, line);
917
918 if !is_definition_like(&signature) {
920 continue;
921 }
922
923 let real_name = extract_name_from_signature(&signature);
925 if real_name.len() < 3 {
926 continue;
927 }
928
929 if is_common_word(&real_name) {
931 continue;
932 }
933
934 if real_name.starts_with("__") && real_name.ends_with("__") {
936 continue;
937 }
938
939 let key = (real_name.clone(), file_path.clone());
941 if seen.contains(&key) {
942 continue;
943 }
944 seen.insert(key);
945
946 let doc_comment = read_doc_comment(lines, line);
948
949 symbols.push(Symbol {
950 name: real_name,
951 signature,
952 file: file_path,
953 line,
954 doc_comment,
955 block_id,
956 end_line,
957 indent,
958 });
959 }
960
961 Ok(symbols)
962}
963
964fn extract_name_from_signature(sig: &str) -> String {
969 let cleaned = sig.trim();
970
971 let mut rest = cleaned;
973 let keywords = [
974 "pub(crate) ", "pub(super) ", "pub ", "export ", "async ", "extern ",
975 "static ", "inline ", "virtual ", "override ", "final ", "abstract ",
976 "unsafe ", "mut ",
977 ];
978 loop {
979 let mut changed = false;
980 for kw in &keywords {
981 if rest.starts_with(kw) {
982 rest = &rest[kw.len()..];
983 changed = true;
984 break;
985 }
986 }
987 if !changed {
988 break;
989 }
990 }
991
992 let decl_kws = [
994 "fn ", "function ", "def ", "func ", "struct ", "class ", "enum ",
995 "trait ", "interface ", "type ", "impl ", "const ", "let ", "var ",
996 "val ",
997 ];
998 for kw in &decl_kws {
999 if rest.starts_with(kw) {
1000 rest = &rest[kw.len()..];
1001 break;
1002 }
1003 }
1004
1005 if rest.starts_with("impl ") {
1008 rest = &rest[5..];
1009 }
1010
1011 let end = rest
1013 .find(['(', '<', ':', '{', ' ', '=', ';'])
1014 .unwrap_or(rest.len());
1015 let name = rest[..end].trim();
1016
1017 let name = name.trim_start_matches('\'');
1019
1020 if name.is_empty() || name.len() < 2 {
1021 return String::new();
1022 }
1023
1024 if !name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
1026 return String::new();
1027 }
1028 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1029 return String::new();
1030 }
1031
1032 name.to_string()
1033}
1034
1035fn is_source_file(path: &str) -> bool {
1037 if path.contains("/bench/")
1040 || path.contains("/tests/")
1041 || path.contains("/test/")
1042 || path.contains("/target/")
1043 || path.contains("/vendor/")
1044 || path.contains("/node_modules/")
1045 || path.contains("/__pycache__/")
1046 || path.contains("/.git/")
1047 {
1048 return false;
1049 }
1050 let ext = Path::new(path)
1051 .extension()
1052 .and_then(|e| e.to_str())
1053 .unwrap_or("");
1054 matches!(
1055 ext,
1056 "rs" | "py" | "ts" | "tsx" | "js" | "jsx"
1057 | "go" | "java" | "c" | "h" | "cpp" | "cc" | "cxx"
1058 | "rb" | "swift" | "kt" | "scala" | "clj" | "ex" | "exs"
1059 | "elm" | "fs" | "ml" | "hs" | "lua" | "php" | "pl"
1060 )
1061}
1062
1063fn is_definition_like(line: &str) -> bool {
1067 let stripped = line.trim();
1068 if stripped.is_empty() {
1069 return false;
1070 }
1071
1072 if stripped.starts_with("//")
1074 || stripped.starts_with('#')
1075 || stripped.starts_with("/*")
1076 || stripped.starts_with("///")
1077 || stripped.starts_with("--")
1078 || stripped.starts_with('\'')
1079 {
1080 return false;
1081 }
1082
1083 if stripped.starts_with("if ")
1085 || stripped.starts_with("for ")
1086 || stripped.starts_with("while ")
1087 || stripped.starts_with("return ")
1088 || stripped.starts_with("match ")
1089 || stripped.starts_with("switch ")
1090 || stripped.starts_with("let ")
1091 || stripped.starts_with("let mut")
1092 || stripped.starts_with("const ")
1093 || stripped.starts_with("var ")
1094 || stripped.starts_with("static ") || stripped.starts_with("type ") || stripped.starts_with("enum ") || stripped.starts_with("f.write")
1098 || stripped.starts_with("println")
1099 || stripped.starts_with("print")
1100 || stripped.starts_with("eprintln")
1101 {
1102 return false;
1103 }
1104
1105 let check = stripped.trim_end_matches([';', ',']);
1106
1107 let ends_with_brace = check.ends_with('{')
1119 || (check.ends_with('}')
1120 && check.matches('{').count() == 1
1121 && check.matches('}').count() == 1);
1122 if check.contains('(') && ends_with_brace {
1123 let before_paren = check.split('(').next().unwrap_or("").trim();
1124 let mut name_part = before_paren;
1128 for kw in &["pub(crate) ", "pub(super) ", "pub ", "async ", "unsafe ",
1129 "extern ", "fn ", "function ", "func ", "export ",
1130 "static ", "inline ", "const ", "final ", "override "] {
1131 if name_part.starts_with(kw) {
1132 name_part = &name_part[kw.len()..];
1133 }
1134 }
1135 if !name_part.contains("::")
1138 && !name_part.contains('.')
1139 && !name_part.contains("->")
1140 && !name_part.contains("||")
1141 && !name_part.contains('=')
1142 && !name_part.contains(' ')
1143 && name_part.len() > 3
1144 {
1145 return true;
1146 }
1147 }
1148
1149 if check.starts_with("def ") && check.ends_with(':') {
1151 return true;
1152 }
1153
1154 if check.ends_with('{') {
1156 let lower = check.to_ascii_lowercase();
1157 if lower.contains(" struct ")
1158 || lower.contains(" class ")
1159 || lower.contains(" enum ")
1160 || lower.contains(" trait ")
1161 || lower.contains(" interface ")
1162 || lower.contains(" impl ")
1163 || lower.starts_with("struct ")
1164 || lower.starts_with("class ")
1165 || lower.starts_with("enum ")
1166 || lower.starts_with("trait ")
1167 || lower.starts_with("interface ")
1168 || lower.starts_with("impl ")
1169 || lower.starts_with("pub struct ")
1170 || lower.starts_with("pub enum ")
1171 || lower.starts_with("pub trait ")
1172 {
1173 return true;
1174 }
1175 }
1176
1177 if check.ends_with('{') && (check.starts_with("export interface ") || check.starts_with("interface ") || check.starts_with("export type ") || check.starts_with("type ")) {
1179 return true;
1180 }
1181
1182 false
1183}
1184
1185fn is_common_word(word: &str) -> bool {
1187 if word.len() < 3 {
1189 return true;
1190 }
1191 const COMMON: &[&str] = &[
1193 "the", "and", "for", "not", "are", "but", "you", "all", "can", "her",
1194 "was", "one", "our", "out", "has", "his", "how", "its", "may", "new",
1195 "now", "old", "see", "way", "who", "did", "get", "let", "say", "she",
1196 "too", "use", "any", "ask", "bad", "big", "day", "end", "few", "got",
1197 "had", "him", "his", "how", "its", "man", "men", "put", "run", "set",
1198 "try", "two", "use", "via", "yet", "also", "code", "data", "file",
1199 "from", "have", "into", "just", "like", "make", "more", "must",
1200 "only", "over", "some", "such", "than", "them", "then", "they",
1201 "this", "that", "when", "what", "where", "which", "while", "will",
1202 "with", "work", "would", "your", "about", "after", "again", "before",
1203 "being", "below", "could", "every", "first", "found", "great",
1204 "group", "having", "here", "high", "into", "itself", "just",
1205 "large", "last", "left", "like", "long", "made", "many", "most",
1206 "much", "never", "next", "once", "other", "part", "place", "right",
1207 "same", "should", "show", "since", "small", "some", "still",
1208 "such", "take", "their", "them", "then", "there", "these",
1209 "they", "thing", "think", "this", "those", "through", "time",
1210 "under", "until", "very", "well", "were", "what", "when",
1211 "where", "which", "while", "will", "with", "without", "would",
1212 "write", "your", "index", "free", "grammar", "instead", "tool",
1213 "tree", "language", "read", "work", "reliary",
1214 ];
1215 static COMMON_SET: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
1217 let set = COMMON_SET.get_or_init(|| std::collections::HashSet::from_iter(COMMON.iter().copied()));
1218 let lower = word.to_ascii_lowercase();
1219 set.contains(lower.as_str())
1220}
1221
1222fn read_signature_line(lines: &[String], line: i32) -> String {
1223 lines
1224 .get(line.max(0) as usize)
1225 .map(|s| s.trim().to_string())
1226 .unwrap_or_default()
1227}
1228
1229fn read_doc_comment(lines: &[String], def_line: i32) -> String {
1230 let def_idx = (def_line.max(0) as usize).min(lines.len().saturating_sub(1));
1231
1232 let mut doc_parts: Vec<String> = Vec::new();
1234 let mut j = def_idx.saturating_sub(1);
1235
1236 if j < lines.len() && lines[j].trim().ends_with("*/") {
1238 let mut block: Vec<String> = Vec::new();
1239 while j > 0 {
1240 let l = lines[j].trim();
1241 block.insert(0, l.to_string());
1242 if l.starts_with("/**") || l.starts_with("/*") {
1243 break;
1244 }
1245 j -= 1;
1246 }
1247 for bl in &block {
1248 let clean = bl
1249 .trim()
1250 .trim_start_matches("/**")
1251 .trim_start_matches("/*")
1252 .trim_end_matches("*/")
1253 .trim()
1254 .trim_start_matches('*')
1255 .trim();
1256 if !clean.is_empty() && !clean.starts_with('@') && clean != "/**" {
1257 doc_parts.insert(0, clean.to_string());
1258 }
1259 }
1260 }
1261
1262 if doc_parts.is_empty() {
1263 j = def_idx.saturating_sub(1);
1265 while j > 0 {
1266 let l = lines[j].trim();
1267 if let Some(rest) = l.strip_prefix("///") {
1268 doc_parts.insert(0, rest.trim().to_string());
1269 } else if let Some(rest) = l.strip_prefix("//") {
1270 doc_parts.insert(0, rest.trim().to_string());
1271 } else if l.starts_with('#') && !l.starts_with("#[") {
1272 doc_parts.insert(0, l[1..].trim().to_string());
1273 } else if l.is_empty() {
1274 } else {
1276 break;
1277 }
1278 j -= 1;
1279 }
1280 }
1281
1282 doc_parts.join(" ")
1283}
1284
1285fn is_noise_line(line: &str) -> bool {
1297 let trimmed = line.trim();
1298 if trimmed.is_empty() {
1299 return true;
1300 }
1301 if trimmed.len() < 15 {
1304 return true;
1305 }
1306 if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1308 return true;
1309 }
1310 if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1313 return true;
1314 }
1315 if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1317 return true;
1318 }
1319 if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1321 return true;
1322 }
1323 if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1325 return true;
1326 }
1327 if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1329 return true;
1330 }
1331 if trimmed.starts_with("assert") {
1333 return true;
1334 }
1335 if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1337 return true;
1338 }
1339 if (trimmed.starts_with("let ") || trimmed.starts_with("let mut "))
1343 && !trimmed.contains("(")
1344 && !trimmed.contains("{")
1345 && trimmed.ends_with(";")
1346 {
1347 return true;
1348 }
1349 let trimmed_for_return = if let Some(comment_start) = trimmed.find("//") {
1353 trimmed[..comment_start].trim()
1354 } else {
1355 trimmed
1356 };
1357 if let Some(rest) = trimmed_for_return.strip_prefix("return ") {
1358 if let Some(inner) = rest.strip_suffix(";") {
1361 let inner = inner.trim();
1362 if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1363 return true;
1365 }
1366 }
1367 if rest.contains(".lower()") || rest.contains(".upper()") {
1369 return true;
1370 }
1371 }
1372 if trimmed.starts_with("if isinstance(") {
1374 return true;
1375 }
1376 if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1378 return true;
1379 }
1380 if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1382 return true;
1383 }
1384 if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1386 return true;
1387 }
1388 if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1390 return true;
1391 }
1392 if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1394 return true;
1395 }
1396 if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1399 return true;
1400 }
1401 if let Some(rest) = trimmed.strip_prefix("self.") {
1403 if rest.starts_with("assert") || rest.contains(".assert") || rest.starts_with("setUp")
1404 || rest.starts_with("tearDown") || rest.starts_with("set_up")
1405 || rest.starts_with("tear_down") || rest.starts_with("skip(")
1406 || rest.starts_with("fixture") || rest.starts_with("patch(")
1407 {
1408 return true;
1409 }
1410 }
1411 if trimmed.contains(".append(") || trimmed.contains(".push(")
1414 || trimmed.contains(".insert(") || trimmed.contains(".remove(")
1415 || trimmed.contains(".clear(") || trimmed.contains(".drop(")
1416 || trimmed.contains(".len()") || trimmed.contains(".is_empty()")
1417 || trimmed.contains(".expect(") || trimmed.contains(".collect()")
1418 || trimmed.contains(".clone(") || trimmed.contains(".iter()")
1419 {
1420 return true;
1421 }
1422 if trimmed == "break;" || trimmed == "continue;"
1424 || trimmed == "break" || trimmed == "continue" {
1425 return true;
1426 }
1427 if trimmed.starts_with("from ") {
1429 return true;
1430 }
1431 if let Some(stripped_return) = trimmed.strip_prefix("return ") {
1433 let rest = stripped_return.trim().trim_end_matches(';').trim();
1434 if matches!(rest, "true" | "false" | "None" | "0" | "1" | "Self" | "-1") {
1435 return true;
1436 }
1437 }
1438 if trimmed.starts_with("log::") || trimmed.starts_with("tracing::")
1440 || trimmed.starts_with("debug!") || trimmed.starts_with("info!")
1441 || trimmed.starts_with("warn!") || trimmed.starts_with("error!")
1442 || trimmed.starts_with("print!") || trimmed.starts_with("eprint!")
1443 || trimmed.starts_with("println!") || trimmed.starts_with("eprintln!")
1444 || trimmed.starts_with("dbg!") || trimmed.starts_with("todo!")
1445 {
1446 return true;
1447 }
1448 if trimmed.starts_with("let _ =") || trimmed.starts_with("let _mut _ =")
1450 || trimmed.starts_with("let (_, _) =") || trimmed.starts_with("let (_, _, _) =")
1451 {
1452 return true;
1453 }
1454 let code_only = if let Some(comment_start) = trimmed.find("//") {
1457 trimmed[..comment_start].trim()
1458 } else {
1459 trimmed
1460 };
1461 if code_only.ends_with(".ok()?") || code_only.ends_with(".ok();")
1462 || code_only.contains(".expect(\"") || code_only.contains(".unwrap_or(")
1463 || code_only.contains(".unwrap_or_default(") || code_only.contains(".unwrap_or_else(")
1464 || (code_only.starts_with("return ") && code_only.contains(".ok()"))
1465 || (code_only.starts_with("return ") && code_only.contains(".err()"))
1466 || (code_only.starts_with("return ") && code_only.contains(".unwrap()"))
1467 {
1468 return true;
1469 }
1470 if let Some(eq_pos) = trimmed.find('=') {
1471 if eq_pos > 0 && !trimmed[eq_pos+1..].trim().starts_with("\"")
1472 && trimmed.contains(".to_string(")
1473 && !trimmed.contains("unsafe ") && !trimmed.contains("transmute")
1474 {
1475 return true;
1476 }
1477 }
1478 false
1479}
1480
1481fn extract_surprise_from_body(
1487 body: &str,
1488 skeleton_freq: &FxHashMap<String, usize>,
1489) -> Vec<String> {
1490 let mut surprises: Vec<String> = Vec::new();
1491 let mut seen: HashSet<String> = HashSet::new();
1492
1493 let lines: Vec<&str> = body.lines().collect();
1494
1495 if let Some(m) = regex_capture(r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{", body) {
1502 add_surprise(&mut surprises, &mut seen,
1503 format!("auto-pass for input below {} chars (no gates applied)", m));
1504 }
1505
1506 if body.contains("expect_dash") || body.contains("j == 8") && body.contains("j == 13") && body.contains("j == 23") {
1509 add_surprise(&mut surprises, &mut seen,
1510 "UUID dash positions: 8, 13, 18, 23 (standard 8-4-4-4-12)".to_string());
1511 }
1512
1513 if body.contains("he - i") || body.contains("hex_len") {
1516 if let Some(m) = regex_capture(r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)", body) {
1517 add_surprise(&mut surprises, &mut seen,
1518 format!("hex-hash length range: {} to {}", m.split(',').next().unwrap_or(""), m.split(',').nth(1).unwrap_or("")));
1519 }
1520 }
1521
1522 if body.contains("5381") && body.contains("wrapping_mul") && body.contains("33") {
1525 add_surprise(&mut surprises, &mut seen,
1526 "djb3-33 hash: starts at 5381, multiplies by 33 (Bernstein variant)".to_string());
1527 }
1528
1529 if body.contains("bytes[") && body.contains("while") && !body.contains("chars()") {
1532 add_surprise(&mut surprises, &mut seen,
1533 "byte-indexed processing (not char-indexed)".to_string());
1534 }
1535
1536 if let Some(m) = regex_capture(r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)", body) {
1539 add_surprise(&mut surprises, &mut seen,
1540 format!("sentinel return: {} for empty input", m));
1541 }
1542
1543if body.contains("import") && body.contains("count") && body.contains('[') {
1546 add_surprise(&mut surprises, &mut seen,
1547 "imports collapsed into a single label '[N imports]'".to_string());
1548 }
1549
1550 if body.contains("flush") || (body.contains("if ") && body.contains("emit") && body.contains("reset")) {
1553 add_surprise(&mut surprises, &mut seen,
1554 "lazy flush: label emitted on next non-matching line, not immediately".to_string());
1555 }
1556
1557 if body.contains("Error") && body.contains("Comment") {
1560 let error_pos = body.find("Error").unwrap_or(0);
1561 let comment_pos = body.find("Comment").unwrap_or(0);
1562 if error_pos < comment_pos && error_pos > 0 && comment_pos > 0 {
1563 add_surprise(&mut surprises, &mut seen,
1564 "Error check runs BEFORE Comment check (priority order)".to_string());
1565 } else if comment_pos < error_pos && comment_pos > 0 && error_pos > 0 {
1566 add_surprise(&mut surprises, &mut seen,
1567 "Comment check runs BEFORE Error check (potential bug: errors in comments are missed)".to_string());
1568 }
1569 }
1570
1571 if regex_capture(r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+", body).is_some() {
1574 if body.contains("&&") {
1575 add_surprise(&mut surprises, &mut seen,
1576 "gates are AND-combined: ALL must fail for None return".to_string());
1577 } else if body.contains("||") {
1578 add_surprise(&mut surprises, &mut seen,
1579 "gates are OR-combined: ANY failing gate returns None".to_string());
1580 }
1581 }
1582
1583 if body.contains("aggressive") || body.contains("collapse") {
1586 if let Some(m) = regex_capture(r"\w+\s*>\s*\w+\s*\+\s*(\d+)", body) {
1587 add_surprise(&mut surprises, &mut seen,
1588 format!("minimum word length threshold: {} (below this, words kept verbatim)", m));
1589 }
1590 }
1591
1592 if let Some(m) = regex_capture(r"let\s+(\w+)\s*=\s*skeleton\(\w+\)", body) {
1595 add_surprise(&mut surprises, &mut seen,
1596 format!("operates on the SKELETON of input, not the original text (via {})", m));
1597 }
1598
1599 if body.contains("replace_all") && body.contains("&") {
1602 let placeholders: Vec<&str> = body
1603 .split(".replace_all")
1604 .skip(1)
1605 .filter_map(|s| {
1606 if let Some(idx) = s.find('"') {
1607 let after = &s[idx..];
1608 if let Some(end) = after[1..].find('"') {
1609 return Some(&after[1..=end]);
1610 }
1611 }
1612 None
1613 })
1614 .collect();
1615 if !placeholders.is_empty() {
1616 add_surprise(&mut surprises, &mut seen,
1617 format!("regex-based replacement: {}", placeholders.iter().take(3).cloned().collect::<Vec<_>>().join(", ")));
1618 }
1619 }
1620
1621if body.contains("num_end") && body.contains('.') {
1624 add_surprise(&mut surprises, &mut seen,
1625 "version detection: requires exactly three dot-separated numeric components (X.Y.Z)".to_string());
1626 }
1627
1628 let mut seen_nums: HashSet<String> = HashSet::new();
1630 let mut seen_conds: HashSet<String> = HashSet::new();
1631
1632 for (line_idx, line) in lines.iter().enumerate() {
1633 let trimmed = line.trim();
1634 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#")
1635 || trimmed.starts_with("///") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
1636 continue;
1637 }
1638 let code = trimmed;
1639
1640 for num in find_magic_numbers(code) {
1642 let key = format!("{}_{}", num, line_idx);
1643 if !seen_nums.contains(&key) {
1644 seen_nums.insert(key);
1645 let labeled = label_number(&num, code);
1646 if !labeled.starts_with("constant: 0") && !labeled.starts_with("constant: 1") {
1647 add_surprise(&mut surprises, &mut seen, labeled);
1648 }
1649 }
1650 }
1651
1652 if code.contains("if ") && (code.contains("&&") || code.contains("||")) {
1654 if let Some(cond) = extract_condition(code) {
1655 let cond_key: String = cond.chars().take(40).collect();
1656 if cond.len() > 10 && cond.len() < 200 && !seen_conds.contains(&cond_key) {
1657 seen_conds.insert(cond_key);
1658 add_surprise(&mut surprises, &mut seen,
1659 format!("compound condition: {}", cond));
1660 }
1661 }
1662 }
1663
1664 if line_idx < 15 && code.contains("if ") && code.contains("return") {
1666 if let Some(ret) = extract_return_value(code) {
1667 if !ret.is_empty() && ret != "true" && ret != "false" {
1668 add_surprise(&mut surprises, &mut seen,
1669 format!("early return → {}", ret));
1670 }
1671 }
1672 }
1673
1674 if code.contains(".wrapping_") {
1676 let ops: Vec<&str> = code
1677 .split(".wrapping_")
1678 .skip(1)
1679 .filter_map(|s| s.split('(').next())
1680 .collect();
1681 if !ops.is_empty() {
1682 add_surprise(&mut surprises, &mut seen,
1683 format!("wrapping: {}", ops.join(", ")));
1684 }
1685 }
1686 if code.contains("unwrap_or(") {
1687 if let Some(def) = extract_unwrap_default(code) {
1688 if !def.is_empty() {
1689 add_surprise(&mut surprises, &mut seen,
1690 format!("fallback: unwrap_or({})", def));
1691 }
1692 }
1693 }
1694
1695 if code.contains("with_capacity(") {
1697 if let Some(n) = extract_capacity(code) {
1698 add_surprise(&mut surprises, &mut seen,
1699 format!("capacity: {}", n));
1700 }
1701 }
1702
1703 if let Some(comment_start) = code.find("//") {
1705 let comment = code[comment_start + 2..].trim().to_ascii_lowercase();
1706 let comment_text = code[comment_start + 2..].trim();
1707 for sig in ["key", "difference", "note", "important", "surprise",
1708 "critical", "deliberate", "only fires", "not regex", "must"]
1709 {
1710 if comment.contains(sig) {
1711 add_surprise(&mut surprises, &mut seen,
1712 format!("source note: {}", comment_text));
1713 break;
1714 }
1715 }
1716 }
1717 }
1718
1719 for (line_idx, line) in lines.iter().enumerate() {
1721 if line_idx == 0 { continue; } let trimmed = line.trim();
1723 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
1724 continue;
1725 }
1726 if is_noise_line(trimmed) {
1727 continue;
1728 }
1729 let skel = aggressive_skeleton(trimmed);
1730 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 10 {
1731 continue;
1732 }
1733 let freq = skeleton_freq.get(&skel).copied().unwrap_or(0);
1734 if freq == 1 {
1735 let has_signal = trimmed.contains('(')
1740 || trimmed.contains("==") || trimmed.contains("!=")
1741 || trimmed.contains(" < ") || trimmed.contains(" > ")
1742 || trimmed.contains(">=") || trimmed.contains("<=")
1743 || trimmed.contains("&&") || trimmed.contains("||")
1744 || trimmed.contains("wrapping_") || trimmed.contains("unwrap_or")
1745 || trimmed.contains("match ") || trimmed.contains("=>");
1746 if has_signal {
1747 let snippet: String = trimmed.chars().take(80).collect();
1748 add_surprise(&mut surprises, &mut seen,
1749 format!("unique line: {}", snippet));
1750 }
1751 }
1752 }
1753
1754 let pre_dedup_len = surprises.len();
1757 let mut final_surprises: Vec<String> = Vec::new();
1758 let mut seen_fragments: HashSet<String> = HashSet::new();
1759 for s in surprises {
1760 let fragment: String = s
1762 .chars()
1763 .filter(|c| !c.is_whitespace())
1764 .take(40)
1765 .collect();
1766 if seen_fragments.contains(&fragment) {
1767 continue;
1768 }
1769 seen_fragments.insert(fragment);
1770 final_surprises.push(s);
1771 }
1772 let surprises = final_surprises;
1773 let post_dedup_len = surprises.len();
1774 if pre_dedup_len > post_dedup_len + 1 {
1775 }
1777 let pattern_count = surprises.iter()
1779 .filter(|s| !s.starts_with("rare pattern") && !s.starts_with("unique line"))
1780 .count();
1781 let freq_count = surprises.len() - pattern_count;
1782
1783 let limit = if pattern_count >= 3 {
1787 pattern_count.min(8) } else if pattern_count >= 1 {
1789 (pattern_count + freq_count.min(2)).min(8)
1790 } else {
1791 0
1793 };
1794
1795 surprises.into_iter().take(limit).collect()
1796}
1797
1798fn add_surprise(surprises: &mut Vec<String>, seen: &mut HashSet<String>, s: String) {
1799 let key: String = s.chars().take(60).collect();
1800 if !seen.contains(&key) {
1801 seen.insert(key);
1802 surprises.push(s);
1803 }
1804}
1805
1806fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1809 if pattern == r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{" {
1811 return manual_capture_len_guard(text);
1812 }
1813 if pattern == r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)" {
1814 return manual_capture_range(text);
1815 }
1816 if pattern == r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)" {
1817 return manual_capture_sentinel(text);
1818 }
1819 if pattern == r"\w+\s*>\s*\w+\s*\+\s*(\d+)" {
1820 return manual_capture_gt_plus(text);
1821 }
1822 if pattern == r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+" {
1823 return if text.contains("_threshold") { Some("found".to_string()) } else { None };
1824 }
1825 if pattern == r"let\s+(\w+)\s*=\s*skeleton\(\w+\)" {
1826 return manual_capture_skeleton_let(text);
1827 }
1828 None
1829}
1830
1831fn manual_capture_len_guard(text: &str) -> Option<String> {
1832 let bytes = text.as_bytes();
1833 let mut i = 0;
1834 while i < bytes.len() {
1835 if i + 2 < bytes.len() && bytes[i] == b'i' && bytes[i+1] == b'f' {
1836 i += 2;
1838 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1839 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1841 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; } }
1843 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1845 if i < bytes.len() && bytes[i] == b'<' { i += 1; }
1846 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1847 let start = i;
1849 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1850 if i > start {
1851 return Some(String::from_utf8_lossy(&bytes[start..i]).to_string());
1852 }
1853 }
1854 i += 1;
1855 }
1856 None
1857}
1858
1859fn manual_capture_range(text: &str) -> Option<String> {
1860 let bytes = text.as_bytes();
1861 let mut i = 0;
1862 while i < bytes.len() {
1863 if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i+1] == b'=' {
1864 i += 2;
1865 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1866 let start = i;
1867 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1868 if i > start {
1869 let first = String::from_utf8_lossy(&bytes[start..i]).to_string();
1870 while i < bytes.len() && bytes[i] != b'<' { i += 1; }
1872 if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i+1] == b'=' { i += 2; }
1873 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1874 let start2 = i;
1875 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1876 if i > start2 {
1877 let second = String::from_utf8_lossy(&bytes[start2..i]).to_string();
1878 return Some(format!("{},{}", first, second));
1879 }
1880 }
1881 }
1882 i += 1;
1883 }
1884 None
1885}
1886
1887fn manual_capture_sentinel(text: &str) -> Option<String> {
1888 if let Some(idx) = text.find("is_empty") {
1889 let after = &text[idx + 9..];
1890 if let Some(ret_idx) = after.find("return") {
1891 let after_ret = &after[ret_idx + 6..];
1892 let end = after_ret
1893 .find([';', '}', '\n'])
1894 .unwrap_or(after_ret.len());
1895 return Some(after_ret[..end].trim().to_string());
1896 }
1897 }
1898 None
1899}
1900
1901fn manual_capture_gt_plus(text: &str) -> Option<String> {
1902 if let Some(idx) = text.find('>') {
1903 let after = &text[idx + 1..];
1904 let trimmed = after.trim_start();
1906 let after_ident = trimmed
1908 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1909 let trimmed2 = after_ident.trim_start();
1910 if let Some(rest) = trimmed2.strip_prefix('+') {
1911 let trimmed3 = rest.trim_start();
1912 let end = trimmed3
1913 .find(|c: char| !c.is_ascii_digit())
1914 .unwrap_or(trimmed3.len());
1915 if end > 0 {
1916 return Some(trimmed3[..end].to_string());
1917 }
1918 }
1919 }
1920 None
1921}
1922
1923fn manual_capture_skeleton_let(text: &str) -> Option<String> {
1924 if let Some(idx) = text.find("let ") {
1925 let after = &text[idx + 4..];
1926 let after_ident = after
1927 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1928 let trimmed = after_ident.trim_start();
1929 if let Some(after_eq) = trimmed.strip_prefix('=') {
1930 let after_eq = after_eq.trim_start();
1931 if let Some(sk_idx) = after_eq.find("skeleton(") {
1932 let before = &after_eq[..sk_idx].trim();
1933 if !before.is_empty() {
1934 return Some(before.to_string());
1935 }
1936 }
1937 }
1938 }
1939 None
1940}
1941
1942fn find_magic_numbers(code: &str) -> Vec<String> {
1943 let mut nums = Vec::new();
1944 let bytes = code.as_bytes();
1946 let mut i = 0;
1947 while i < bytes.len() {
1948 if (bytes[i] == b'>' || bytes[i] == b'<' || bytes[i] == b'=' || bytes[i] == b'!')
1949 && i + 1 < bytes.len()
1950 {
1951 i += 1;
1953 if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
1954 i += 1;
1955 }
1956 while i < bytes.len() && bytes[i] == b' ' {
1958 i += 1;
1959 }
1960 let start = i;
1962 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
1963 i += 1;
1964 }
1965 if i > start {
1966 let num = String::from_utf8_lossy(&bytes[start..i]).to_string();
1967 if num != "0" && num != "1" {
1968 nums.push(num);
1969 }
1970 }
1971 } else {
1972 i += 1;
1973 }
1974 }
1975 nums
1976}
1977
1978fn label_number(num: &str, code: &str) -> String {
1979 let lower = code.to_ascii_lowercase();
1980 if lower.contains("uuid") || (num == "8" || num == "13" || num == "18" || num == "23") {
1981 format!("UUID position: {}", num)
1982 } else if lower.contains("hash") || lower.contains("hex") {
1983 format!("hex-hash bound: {}", num)
1984 } else if lower.contains("entropy") || lower.contains("threshold") {
1985 format!("threshold: {}", num)
1986 } else if lower.contains("len") || lower.contains("length") || lower.contains("count") {
1987 format!("length: {}", num)
1988 } else if lower.contains("capacity") {
1989 format!("capacity: {}", num)
1990 } else {
1991 format!("constant: {}", num)
1992 }
1993}
1994
1995fn extract_condition(code: &str) -> Option<String> {
1996 let if_pos = code.find("if ")?;
1997 let after = &code[if_pos + 3..];
1998 let end = after
2000 .find(['{', '\n'])
2001 .unwrap_or(after.len());
2002 Some(after[..end].trim().to_string())
2003}
2004
2005fn extract_return_value(code: &str) -> Option<String> {
2006 let ret_pos = code.find("return")?;
2007 let after = &code[ret_pos + 6..];
2008 let end = after.find([';', '}']).unwrap_or(after.len());
2009 Some(after[..end].trim().to_string())
2010}
2011
2012fn extract_unwrap_default(code: &str) -> Option<String> {
2013 let pos = code.find("unwrap_or(")?;
2014 let after = &code[pos + 10..];
2015 let end = after.find(')')?;
2016 Some(after[..end].trim().to_string())
2017}
2018
2019fn extract_capacity(code: &str) -> Option<String> {
2020 let pos = code.find("with_capacity(")?;
2021 let after = &code[pos + 14..];
2022 let end = after.find(')')?;
2023 Some(after[..end].trim().to_string())
2024}
2025
2026fn build_cross_refs_from_index(
2036 db: &Connection,
2037 symbols: &[Symbol],
2038) -> FxHashMap<String, Vec<String>> {
2039 build_cross_refs_inner(db, symbols).unwrap_or_default()
2041}
2042
2043fn build_cross_refs_inner(
2044 db: &Connection,
2045 symbols: &[Symbol],
2046) -> Result<FxHashMap<String, Vec<String>>, rusqlite::Error> {
2047 let def_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2048 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
2049 let mut ref_set: FxHashMap<String, HashSet<String>> = FxHashMap::default();
2050
2051 let mut all_pairs_stmt = db.prepare(
2056 "SELECT o.file_id, p.phrase, o.is_def
2057 FROM occurrence o
2058 JOIN phrases p ON o.phrase_id = p.id
2059 WHERE length(p.phrase) BETWEEN 3 AND 30
2060 ORDER BY o.file_id
2061 LIMIT 200000",
2062 )?;
2063
2064 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
2066 for row in all_pairs_stmt
2067 .query_map([], |row| {
2068 Ok((
2069 row.get::<_, i64>(0)?,
2070 row.get::<_, String>(1)?,
2071 row.get::<_, i64>(2)? != 0,
2072 ))
2073 })?
2074 .flatten()
2075 {
2076 let (file_id, phrase, is_def) = row;
2077 file_to_phrases
2078 .entry(file_id)
2079 .or_default()
2080 .push((phrase, is_def));
2081 }
2082
2083 let mut phrase_to_non_def_files: FxHashMap<String, Vec<i64>> = FxHashMap::default();
2085 for (file_id, pairs) in &file_to_phrases {
2086 for (phrase, is_def) in pairs {
2087 if !*is_def {
2088 phrase_to_non_def_files
2089 .entry(phrase.clone())
2090 .or_default()
2091 .push(*file_id);
2092 }
2093 }
2094 }
2095
2096 for sym in symbols {
2098 let mut refs: HashSet<String> = HashSet::new();
2099
2100 let files_using_sym = match phrase_to_non_def_files.get(&sym.name) {
2101 Some(f) => f,
2102 None => continue,
2103 };
2104
2105 for &file_id in files_using_sym {
2106 if let Some(pairs) = file_to_phrases.get(&file_id) {
2107 for (phrase, is_def) in pairs {
2108 if *is_def
2109 && phrase != &sym.name
2110 && def_names.contains(phrase.as_str())
2111 && !is_common_word(phrase)
2112 && phrase.len() >= 3
2113 && phrase.len() < 30
2114 && !phrase.starts_with('_')
2115 && !phrase.ends_with("Error")
2116 && !phrase.ends_with("Result")
2117 && !phrase.ends_with("Option")
2118 && phrase != "Self"
2119 {
2120 refs.insert(phrase.clone());
2121 }
2122 }
2123 }
2124
2125 if refs.len() >= 5 {
2127 break;
2128 }
2129 }
2130
2131 if !refs.is_empty() {
2133 let mut sorted: Vec<String> = refs.into_iter().collect();
2134 sorted.sort();
2135 sorted.truncate(3);
2136 ref_set.insert(sym.name.clone(), sorted.into_iter().collect());
2137 }
2138 }
2139
2140 for (name, refs) in ref_set {
2142 let mut sorted: Vec<String> = refs.into_iter().collect();
2143 sorted.sort();
2144 sorted.truncate(3);
2145 cross_refs.insert(name, sorted);
2146 }
2147
2148 Ok(cross_refs)
2149}
2150
2151fn read_symbols_for_subset(
2159 selected: &[Symbol],
2160 _all_symbols: &[Symbol],
2161) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2162 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2163 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2164
2165 for sym in selected {
2169 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2170 meta.lines.to_vec()
2171 } else {
2172 std::fs::read_to_string(&sym.file)
2174 .map(|c| c.lines().map(String::from).collect())
2175 .unwrap_or_default()
2176 };
2177
2178 let start = (sym.line as usize).min(lines.len().saturating_sub(1));
2179 let end = find_body_end(lines.as_slice(), start, sym.indent).min(lines.len());
2180 if start < end {
2181 for line in &lines[start..end] {
2183 let trimmed = line.trim();
2184 if trimmed.is_empty() {
2185 continue;
2186 }
2187 let skel = aggressive_skeleton(trimmed);
2188 if !skel.is_empty() {
2189 *freq.entry(skel).or_insert(0) += 1;
2190 }
2191 }
2192 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2193 }
2194 }
2195
2196 (sources, freq)
2197}
2198
2199fn read_symbols_and_frequency(
2202 symbols: &[Symbol],
2203) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2204 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2205 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2206
2207 for sym in symbols {
2210 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2211 meta.lines.to_vec()
2212 } else {
2213 std::fs::read_to_string(&sym.file)
2214 .map(|c| c.lines().map(String::from).collect())
2215 .unwrap_or_default()
2216 };
2217
2218 let start = (sym.line.max(0) as usize).min(lines.len().saturating_sub(1));
2223 let brace_end = find_body_end(lines.as_slice(), start, sym.indent);
2224 let index_end = (sym.end_line as usize).max(start + 1).min(lines.len());
2225 let end = brace_end.max(index_end).min(lines.len());
2226
2227 if start < end {
2228 for line in &lines[start..end] {
2230 let trimmed = line.trim();
2231 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
2232 continue;
2233 }
2234 let skel = aggressive_skeleton(trimmed);
2235 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 5 {
2236 continue;
2237 }
2238 *freq.entry(skel).or_insert(0) += 1;
2239 }
2240
2241 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2242 }
2243 }
2244
2245 (sources, freq)
2246}
2247
2248fn find_body_end(lines: &[String], start: usize, _base_indent: i32) -> usize {
2253 if start >= lines.len() {
2254 return start;
2255 }
2256 let first_line = &lines[start];
2257
2258 let stripped = first_line.trim();
2260 if stripped.starts_with("def ")
2261 || stripped.starts_with("class ")
2262 || stripped.starts_with("async def ")
2263 {
2264 let first_indent = first_line.len() - first_line.trim_start().len();
2265 for (i, line) in lines.iter().enumerate().skip(start + 1) {
2266 let trimmed = line.trim();
2267 if trimmed.is_empty() || trimmed.starts_with("#") {
2268 continue;
2269 }
2270 let indent = line.len() - line.trim_start().len();
2271 if indent <= first_indent
2273 && (trimmed.starts_with("def ")
2274 || trimmed.starts_with("class ")
2275 || trimmed.starts_with("async def "))
2276 {
2277 return i;
2278 }
2279 }
2280 return lines.len();
2281 }
2282
2283 let mut depth: i32 = 0;
2285 let mut seen_open = false;
2286 for (i, line) in lines.iter().enumerate().skip(start) {
2287 let trimmed = line.trim();
2288
2289 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
2291 continue;
2292 }
2293
2294 for ch in line.chars() {
2295 match ch {
2296 '{' => { depth += 1; seen_open = true; }
2297 '}' => { depth -= 1; }
2298 _ => {}
2299 }
2300 }
2301
2302 if seen_open && depth == 0 {
2303 return i + 1;
2304 }
2305 }
2306 lines.len()
2307}
2308
2309fn derive_crate_name(file_path: &str) -> String {
2316 let parts: Vec<&str> = file_path.split('/').collect();
2317 for (i, part) in parts.iter().enumerate() {
2318 if (*part == "crates" || *part == "packages") && i + 1 < parts.len() {
2319 return parts[i + 1].to_string();
2320 }
2321 }
2322 if parts.len() >= 2 {
2324 return parts[parts.len() - 2].to_string();
2325 }
2326 String::new()
2327}
2328
2329fn detect_modules(symbols: &[Symbol]) -> Vec<(String, Vec<Symbol>)> {
2330 let mut module_map: FxHashMap<String, Vec<Symbol>> = FxHashMap::default();
2331
2332 for sym in symbols {
2333 let module = derive_module_name(&sym.file);
2334 module_map.entry(module).or_default().push(sym.clone());
2335 }
2336
2337 let mut modules: Vec<(String, Vec<Symbol>)> = module_map.into_iter().collect();
2338 modules.sort_by(|a, b| a.0.cmp(&b.0));
2339 modules
2340}
2341
2342fn derive_module_name(file_path: &str) -> String {
2343 let path = Path::new(file_path);
2344 let components: Vec<_> = path.components().collect();
2345
2346 for (i, comp) in components.iter().enumerate() {
2348 let part = comp.as_os_str().to_string_lossy().to_string();
2349 if part == "packages" && i + 1 < components.len() {
2350 return format!("packages/{}", components[i + 1].as_os_str().to_string_lossy());
2351 }
2352 if part == "src" && i + 1 < components.len() {
2353 return format!("src/{}", components[i + 1].as_os_str().to_string_lossy());
2354 }
2355 if part == "crates" && i + 1 < components.len() {
2356 return format!("crates/{}", components[i + 1].as_os_str().to_string_lossy());
2357 }
2358 }
2359
2360 components
2362 .first()
2363 .map(|c| c.as_os_str().to_string_lossy().to_string())
2364 .unwrap_or_else(|| "root".to_string())
2365}
2366
2367fn build_cochange_line(_name: &str, signature: &str, _body: &str, cross_refs: &[String]) -> String {
2373 let lower = signature.to_ascii_lowercase();
2374 let mut parts: Vec<String> = Vec::new();
2375 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2376 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2377
2378 if is_enum {
2379 if !cross_refs.is_empty() {
2380 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2381 parts.push(format!("Add variant → update match sites: {}", sites.join(", ")));
2382 }
2383 } else if is_struct {
2384 if !cross_refs.is_empty() {
2385 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2386 parts.push(format!("Add field → update constructors: {}", sites.join(", ")));
2387 }
2388 } else {
2389 if !cross_refs.is_empty() {
2390 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2391 parts.push(format!("Rename → update {} caller(s): {}", cross_refs.len(), sites.join(", ")));
2392 }
2393 }
2394 parts.join("; ")
2395}
2396
2397fn expand_type_signature(signature: &str, body: &str) -> String {
2398 let sig_trimmed = signature.trim();
2399 let lower = sig_trimmed.to_ascii_lowercase();
2400 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2401 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2402 if !is_enum && !is_struct {
2403 return signature.to_string();
2404 }
2405 let mut names: Vec<String> = Vec::new();
2406 for line in body.lines().skip(1) {
2407 let trimmed = line.trim();
2408 if trimmed == "}" || trimmed.is_empty() { continue; }
2409 if is_enum {
2410 let name = trimmed.split([',', '{', '(', ':']).next().unwrap_or("").trim().trim_end_matches(',');
2411 if !name.is_empty() && name.len() > 1 && name.chars().next().is_some_and(|c| c.is_uppercase() || c == '_') {
2412 names.push(name.to_string());
2413 }
2414 } else if is_struct {
2415 if let Some(colon_pos) = trimmed.find(':') {
2416 let name = trimmed[..colon_pos].trim().trim_start_matches("pub ");
2417 if !name.is_empty() && name.len() > 1 { names.push(name.to_string()); }
2418 }
2419 }
2420 }
2421 if names.is_empty() { return signature.to_string(); }
2422 let kind = if is_enum { "variants" } else { "fields" };
2423 format!("{} // {}({})", sig_trimmed, kind, names.join(", "))
2424}
2425
2426fn render_entry(
2427 sym: &Symbol,
2428 body: &str,
2429 cross_refs: &[String],
2430 skeleton_freq: &FxHashMap<String, usize>,
2431 format: PackFormat,
2432) -> String {
2433 let crate_name = derive_crate_name(&sym.file);
2435 let header = if crate_name.is_empty() {
2436 format!("## {}", sym.name)
2437 } else {
2438 format!("## {}/{}", sym.name, crate_name)
2439 };
2440 let mut parts = vec![header];
2441
2442 if matches!(format, PackFormat::Full) && !sym.doc_comment.is_empty() {
2444 let l0 = sym
2445 .doc_comment
2446 .split('.')
2447 .next()
2448 .unwrap_or(&sym.doc_comment)
2449 .trim();
2450 let l0 = if l0.chars().count() > 120 {
2451 l0.chars().take(120).collect::<String>()
2453 } else {
2454 l0.to_string()
2455 };
2456 parts.push(format!("L0: {}", l0));
2457 }
2458
2459 let l2_line = expand_type_signature(&sym.signature, body);
2461 parts.push(format!("L2: {} [{}:{}]", l2_line, sym.file, sym.line));
2462
2463 if !body.is_empty() {
2465 let surprises = extract_surprise_from_body(body, skeleton_freq);
2466 if !surprises.is_empty() {
2467 parts.push(format!("L3: {}", surprises.join("; ")));
2468 }
2469 }
2470
2471 if matches!(format, PackFormat::Full) && cross_refs.len() >= 2 {
2473 parts.push(format!("Cross-refs: {}", cross_refs.iter().take(5).cloned().collect::<Vec<_>>().join(", ")));
2474 }
2475
2476 let l4 = build_cochange_line(&sym.name, &sym.signature, body, cross_refs);
2478 if !l4.is_empty() {
2479 parts.push(format!("L4: {}", l4));
2480 }
2481
2482 parts.push(String::new());
2483 parts.join("\n")
2484}