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 symbols = extract_symbols_from_index(&db)?;
203
204 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
206
207 let cross_refs = build_cross_refs_from_index(&db, &symbols);
209
210 let mut entries = Vec::new();
212 entries.push(format!(
213 "# Holographic Pack — {} ({} symbols)\n",
214 Path::new(path)
215 .file_name()
216 .map(|n| n.to_string_lossy().to_string())
217 .unwrap_or_else(|| path.to_string()),
218 symbols.len()
219 ));
220
221 for sym in &symbols {
222 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
223 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
224 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
225 entries.push(entry);
226 }
227
228 Ok(entries.join("\n"))
229}
230
231pub fn generate_pack_hotspot(
239 path: &str,
240 format: PackFormat,
241 top_k: usize,
242) -> Result<String, String> {
243 let db = open_index(path)?;
244 generate_pack_hotspot_with(&db, path, format, top_k)
245}
246
247pub fn generate_pack_hotspot_with(
249 db: &Connection,
250 path: &str,
251 format: PackFormat,
252 top_k: usize,
253) -> Result<String, String> {
254 let symbols = extract_symbols_from_index(db)?;
255
256 let inbound_counts = compute_inbound_ref_counts(db, &symbols);
259 let mut scored: Vec<(f32, &Symbol)> = symbols
260 .iter()
261 .map(|s| {
262 let inbound = *inbound_counts.get(&s.name).unwrap_or(&0) as f32;
263 (0.6 * inbound, s) })
265 .collect();
266 let k = top_k.min(scored.len());
268 if k < scored.len() {
269 scored.select_nth_unstable_by(k, |a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
270 }
271 scored.truncate(k);
272
273 let selected: Vec<Symbol> = scored
274 .into_iter()
275 .map(|(_, s)| s.clone())
276 .collect();
277
278 let (symbol_sources, skeleton_freq) = read_symbols_for_subset(&selected, &symbols);
279
280 let cross_refs = build_cross_refs_for_subset(db, &selected, &symbols);
282
283 let mut entries = Vec::with_capacity(selected.len() + 1);
284 entries.push(format!(
285 "# Holographic Pack — {} ({} of {} symbols, hotspot-ranked)\n",
286 Path::new(path)
287 .file_name()
288 .map(|n| n.to_string_lossy().to_string())
289 .unwrap_or_else(|| path.to_string()),
290 selected.len(),
291 symbols.len()
292 ));
293
294 for sym in &selected {
295 let refs = cross_refs.get(&sym.name).map(|v| v.as_slice()).unwrap_or(&[]);
297 let source = symbol_sources.get(&sym.name).map(|s| s.as_str()).unwrap_or("");
298 let entry = render_entry(sym, source, refs, &skeleton_freq, format);
299 entries.push(entry);
300 }
301
302 let result = entries.join("\n");
303 Ok(result)
304}
305
306fn compute_inbound_ref_counts(
310 db: &Connection,
311 symbols: &[Symbol],
312) -> FxHashMap<String, usize> {
313 let mut counts: FxHashMap<String, usize> = FxHashMap::default();
314 let symbol_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
315
316 let mut stmt = match db.prepare(
319 "SELECT p.phrase, COUNT(*) as cnt
320 FROM occurrence o
321 JOIN phrases p ON o.phrase_id = p.id
322 JOIN file_map f ON o.file_id = f.id
323 WHERE o.is_def = 0
324 AND f.file_path NOT LIKE '%/bench/%'
325 AND f.file_path NOT LIKE '%/tests/%'
326 AND f.file_path NOT LIKE '%/test/%'
327 AND f.file_path NOT LIKE '%/target/%'
328 AND f.file_path NOT LIKE '%/vendor/%'
329 AND f.file_path NOT LIKE '%/node_modules/%'
330 AND f.file_path NOT LIKE '%/__pycache__/%'
331 AND f.file_path NOT LIKE '%/.specstory/%'
332 GROUP BY p.phrase",
333 ) {
334 Ok(s) => s,
335 Err(_) => return counts,
336 };
337
338 let rows = stmt.query_map([], |row| {
339 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
340 });
341
342 if let Ok(rows) = rows {
343 for row in rows.flatten() {
344 let (phrase, cnt) = row;
345 if symbol_names.contains(phrase.as_str()) {
347 counts.insert(phrase, cnt as usize);
348 }
349 }
350 }
351
352 counts
353}
354
355fn build_cross_refs_for_subset(
358 db: &Connection,
359 selected: &[Symbol],
360 all_symbols: &[Symbol],
361) -> FxHashMap<String, Vec<String>> {
362 let all_names: HashSet<&str> = all_symbols.iter().map(|s| s.name.as_str()).collect();
363 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
364
365 let mut all_pairs_stmt = match db.prepare(
372 "SELECT o.file_id, p.phrase, o.is_def
373 FROM occurrence o
374 JOIN phrases p ON o.phrase_id = p.id
375 WHERE p.phrase IN (SELECT phrase FROM phrases WHERE length(phrase) BETWEEN 3 AND 30)
376 ORDER BY o.file_id LIMIT 200000",
377 ) {
378 Ok(s) => s,
379 Err(_) => return cross_refs,
380 };
381
382 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
383 for row in all_pairs_stmt
384 .query_map([], |row| {
385 Ok((
386 row.get::<_, i64>(0)?,
387 row.get::<_, String>(1)?,
388 row.get::<_, i64>(2)? != 0,
389 ))
390 })
391 .ok()
392 .into_iter()
393 .flatten()
394 .filter_map(|r| r.ok())
395 {
396 let (file_id, phrase, is_def) = row;
397 file_to_phrases.entry(file_id).or_default().push((phrase, is_def));
398 }
399
400 for sym in selected {
403 let usage_files: Vec<i64> = file_to_phrases
405 .iter()
406 .filter(|(_, pairs)| pairs.iter().any(|(p, is_def)| *p == sym.name && !*is_def))
407 .map(|(fid, _)| *fid)
408 .take(50)
409 .collect();
410
411 let mut refs: HashSet<String> = HashSet::new();
412 for file_id in usage_files {
413 if let Some(pairs) = file_to_phrases.get(&file_id) {
414 for (phrase, is_def) in pairs {
415 if *is_def
416 && *phrase != sym.name
417 && all_names.contains(phrase.as_str())
418 && !is_common_word(phrase)
419 {
420 refs.insert(phrase.clone());
421 }
422 if refs.len() >= 6 {
423 break;
424 }
425 }
426 }
427 if refs.len() >= 6 {
428 break;
429 }
430 }
431
432 cross_refs.insert(sym.name.clone(), refs.into_iter().take(6).collect());
433 }
434
435 cross_refs
436}
437
438pub fn generate_pack_auto(path: &str) -> Result<String, String> {
444 let conn = open_index(path)?;
446 let decision = gate_decision_for(&conn, path)?;
447
448 match decision {
449 GateDecision::Skip => Ok(String::new()),
450 GateDecision::Minimal => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 15),
451 GateDecision::Full => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 50),
452 }
453}
454
455fn open_index(codebase_path: &str) -> Result<rusqlite::Connection, String> {
457 let db_path = format!(
458 "{}/.reliary/index.sqlite",
459 codebase_path.trim_end_matches('/')
460 );
461 let conn = Connection::open(&db_path)
462 .map_err(|e| format!("pack: open {}: {}", db_path, e))?;
463 reliary_search::schema::open_existing_db_safe(&conn)
464 .map_err(|e| format!("index not initialized: {}", e))?;
465 Ok(conn)
466}
467
468#[derive(Debug, Clone)]
470pub struct PackEntry {
471 pub name: String,
472 pub header: String, pub body: String, }
475
476pub fn parse_pack_entries(pack: &str) -> Vec<PackEntry> {
478 let mut entries = Vec::new();
479 let mut current_name: Option<String> = None;
480 let mut current_header: String = String::new();
481 let mut current_body: String = String::new();
482
483 for line in pack.lines() {
484 if line.starts_with("## ") {
485 if let Some(name) = current_name.take() {
487 entries.push(PackEntry {
488 name,
489 header: std::mem::take(&mut current_header),
490 body: std::mem::take(&mut current_body),
491 });
492 }
493 current_name = Some(line.strip_prefix("///").unwrap_or(line).split('/').next().unwrap_or("").trim().to_string());
494 current_header = line.to_string();
495 } else if current_name.is_some() {
496 if !current_body.is_empty() {
497 current_body.push('\n');
498 }
499 current_body.push_str(line);
500 }
501 }
502 if let Some(name) = current_name {
504 entries.push(PackEntry {
505 name,
506 header: current_header,
507 body: current_body,
508 });
509 }
510 entries
511}
512
513fn tokenize(text: &str) -> Vec<String> {
517 reliary_search::tokenize(text)
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum SliceDecision {
535 Slice,
537 Skip,
539}
540
541pub fn should_slice_for_query(query: &str) -> SliceDecision {
545 let text = query.to_ascii_lowercase();
546
547 if text.contains("bug") || text.contains("issue") || text.contains("wrong")
549 || text.contains("incorrect") || text.contains("missing") {
550 return SliceDecision::Slice;
551 }
552 if text.contains("call") || text.contains("caller") || text.contains("who uses")
553 || text.contains("references") || text.contains("called by") {
554 return SliceDecision::Slice;
555 }
556
557 if text.contains("value") || text.contains("threshold") || text.contains("parameter")
559 || text.contains("default") || text.contains("exactly") || text.contains("what number") {
560 return SliceDecision::Slice;
561 }
562
563 let func_indicators = [
567 "find_clusters", "find_clusters_global", "skelhash", "skeleton_hash",
568 "aggressive_skeleton_hash", "maxwell", "maxwellgate", "should_drop",
569 "detect_strategy", "detect_tabular", "detect_json", "callgraph",
570 "is_definition_line", "reliary_sift", "reliary-output",
571 ];
572 for indicator in &func_indicators {
573 if text.contains(indicator) {
574 return SliceDecision::Slice;
575 }
576 }
577
578 SliceDecision::Skip
581}
582
583pub fn adaptive_slice(pack: &str, query: &str, top_k: usize) -> (String, SliceDecision) {
586 let decision = should_slice_for_query(query);
587 match decision {
588 SliceDecision::Slice => (slice_pack_for_query(pack, query, top_k), decision),
589 SliceDecision::Skip => (String::new(), decision),
590 }
591}
592
593pub fn slice_pack_for_query(pack: &str, query: &str, top_k: usize) -> String {
597 let entries = parse_pack_entries(pack);
598 if entries.is_empty() {
599 return String::new();
600 }
601
602 let query_tokens = tokenize(query);
603 if query_tokens.is_empty() {
604 return entries
606 .into_iter()
607 .take(top_k)
608 .map(|e| format!("{}\n{}", e.header, e.body))
609 .collect::<Vec<_>>()
610 .join("\n\n");
611 }
612
613 let n_docs = entries.len() as f64;
615 let mut doc_tokens: Vec<Vec<String>> = Vec::with_capacity(entries.len());
616 for entry in &entries {
617 let combined = format!("{} {}", entry.name, entry.body);
619 doc_tokens.push(tokenize(&combined));
620 }
621
622 let mut df: FxHashMap<String, usize> = FxHashMap::default();
624 for tokens in &doc_tokens {
625 let unique: HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
626 for t in &query_tokens {
627 if unique.contains(t.as_str()) {
628 *df.entry(t.clone()).or_insert(0) += 1;
629 }
630 }
631 }
632
633 let total_len: usize = doc_tokens.iter().map(|d| d.len()).sum();
635 let avgdl = if n_docs > 0.0 {
636 total_len as f64 / n_docs
637 } else {
638 1.0
639 };
640
641 let mut scored: Vec<(f64, &PackEntry)> = Vec::with_capacity(entries.len());
643 for (i, entry) in entries.iter().enumerate() {
644 let doc_len = doc_tokens[i].len() as f64;
645 let mut score = 0.0;
646 let mut tf: FxHashMap<&str, usize> = FxHashMap::default();
648 for t in &doc_tokens[i] {
649 *tf.entry(t.as_str()).or_insert(0) += 1;
650 }
651 for qt in &query_tokens {
652 let d_freq = *df.get(qt).unwrap_or(&0) as f64;
653 if d_freq == 0.0 {
654 continue;
655 }
656 let idf = ((n_docs - d_freq + 0.5) / (d_freq + 0.5) + 1.0).ln();
657 let term_freq = *tf.get(qt.as_str()).unwrap_or(&0) as f64;
658 if term_freq > 0.0 {
659 let bm25 = idf * (term_freq * (1.2 + 1.0))
660 / (term_freq + 1.2 * (1.0 - 0.75 + 0.75 * (doc_len / avgdl)));
661 score += bm25;
662 }
663 }
664 let name_lower = entry.name.to_ascii_lowercase();
666 for qt in &query_tokens {
667 if name_lower == *qt {
668 score += 5.0; } else if name_lower.contains(qt.as_str()) {
670 score += 2.0; }
672 }
673 scored.push((score, entry));
674 }
675
676 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
678
679 let selected: Vec<&PackEntry> = scored
681 .into_iter()
682 .filter(|(s, _)| *s > 0.0)
683 .take(top_k)
684 .map(|(_, e)| e)
685 .collect();
686
687 if selected.is_empty() {
688 return String::new();
689 }
690
691 let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
696 let mut entries_by_name: FxHashMap<&str, &PackEntry> = FxHashMap::default();
698 for e in &entries {
699 entries_by_name.entry(e.name.as_str()).or_insert(e);
700 }
701 let mut expanded: Vec<&PackEntry> = selected.clone();
702 let mut expanded_names: HashSet<String> = selected_names.clone();
703 let mut expansion_count = 0;
704 let max_expansion = 15usize.saturating_sub(top_k.min(15));
705 for entry in &selected {
706 for line in entry.body.lines() {
707 let refs_text = if let Some(rest) = line.strip_prefix("Cross-refs:") {
708 Some(rest.trim())
709 } else if line.starts_with("L4:") && line.contains("caller") {
710 if let Some(pos) = line.find(": ") {
712 let after = &line[pos + 2..];
713 if let Some(last_colon) = after.rfind(": ") {
715 Some(&after[last_colon + 2..])
716 } else {
717 Some(after)
718 }
719 } else {
720 None
721 }
722 } else {
723 None
724 };
725 if let Some(refs) = refs_text {
726 for ref_name in refs.split(',').take(3) {
727 let ref_name = ref_name.trim();
728 if ref_name.is_empty() { continue; }
729 let ref_name_slash = format!("{}/", ref_name);
730 if let Some(e) = entries_by_name.get(ref_name).or_else(|| {
732 entries_by_name.values().find(|e| e.name.starts_with(&ref_name_slash))
733 }) {
734 if !selected_names.contains(&e.name) && !expanded_names.contains(&e.name) {
735 expanded_names.insert(e.name.clone());
736 expanded.push(e);
737 expansion_count += 1;
738 if expansion_count >= max_expansion { break; }
739 }
740 }
741 if expansion_count >= max_expansion { break; }
742 }
743 break; }
745 }
746 if expansion_count >= max_expansion { break; }
747 }
748
749 let total_entries = expanded.len();
750
751 let mut result = format!(
752 "# Sliced Pack — {} entries for query: \"{}\"\n\n",
753 total_entries,
754 query.chars().take(80).collect::<String>()
755 );
756 for entry in &expanded {
757 result.push_str(&format!("{}\n{}\n\n", entry.header, entry.body));
758 }
759 result
760}
761
762pub fn generate_hierarchical_pack(
764 path: &str,
765 format: PackFormat,
766 max_symbols_per_module: usize,
767) -> Result<HierarchicalPack, String> {
768 let db_path = format!(
769 "{}/.reliary/index.sqlite",
770 path.trim_end_matches('/')
771 );
772 let db = Connection::open(&db_path)
773 .map_err(|e| format!("cannot open index: {}", e))?;
774 reliary_search::schema::open_existing_db_safe(&db)
775 .map_err(|e| format!("index not initialized: {}", e))?;
776
777 let symbols = extract_symbols_from_index(&db)?;
778 let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
779 let cross_refs = build_cross_refs_from_index(&db, &symbols);
780
781 let modules = detect_modules(&symbols);
783
784 let mut top_parts = vec![format!(
786 "# Holographic Pack — {} (top-level map)\n",
787 Path::new(path)
788 .file_name()
789 .map(|n| n.to_string_lossy().to_string())
790 .unwrap_or_else(|| path.to_string()),
791 )];
792
793 let mut module_packs: Vec<(String, String)> = Vec::new();
794
795 for (module_name, module_syms) in &modules {
796 let sym_count = module_syms.len();
797 let key_syms: Vec<String> = module_syms
798 .iter()
799 .filter(|s| !s.doc_comment.is_empty())
800 .take(5)
801 .map(|s| s.name.clone())
802 .collect();
803
804 top_parts.push(format!("## {}", module_name));
805 top_parts.push(format!("L0: {} symbols across this module", sym_count));
806 if !key_syms.is_empty() {
807 top_parts.push(format!("Key symbols: {}", key_syms.join(", ")));
808 }
809 top_parts.push(String::new());
810
811 let limited: Vec<&Symbol> = module_syms.iter().take(max_symbols_per_module).collect();
813 if limited.is_empty() {
814 continue;
815 }
816 let mut pack_parts = vec![format!("# Holographic Pack — {}\n", module_name)];
817 for sym in limited {
818 let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
819 let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
820 let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
821 pack_parts.push(entry);
822 }
823 module_packs.push((module_name.clone(), pack_parts.join("\n")));
824 }
825
826 Ok(HierarchicalPack {
827 top_level: top_parts.join("\n"),
828 module_packs,
829 })
830}
831
832pub struct HierarchicalPack {
833 pub top_level: String,
834 pub module_packs: Vec<(String, String)>,
835}
836
837#[derive(Debug, Clone)]
842pub struct Symbol {
843 pub name: String,
844 pub signature: String,
845 pub file: String,
846 pub line: i32,
847 pub doc_comment: String,
848 pub block_id: i64,
849 pub end_line: i32,
850 pub indent: i32,
851}
852
853fn extract_symbols_from_index(db: &Connection) -> Result<Vec<Symbol>, String> {
858 let mut stmt = db
859 .prepare(
860 "SELECT o.phrase_id, o.file_id, o.line, o.block_id,
861 p.phrase, f.file_path,
862 b.end_line, b.indent
863 FROM occurrence o
864 JOIN phrases p ON o.phrase_id = p.id
865 JOIN file_map f ON o.file_id = f.id
866 JOIN block b ON o.block_id = b.block_id
867 WHERE o.is_def = 1
868 ORDER BY f.file_path, o.line",
869 )
870 .map_err(|e| format!("query symbols: {}", e))?;
871
872 let rows = stmt
873 .query_map([], |row| {
874 let phrase: String = row.get(4)?;
875 let file_path: String = row.get(5)?;
876 let line: i32 = row.get(2)?;
877 let block_id: i64 = row.get(3)?;
878 let end_line: i32 = row.get(6)?;
879 let indent: i32 = row.get(7)?;
880 Ok((phrase, file_path, line, block_id, end_line, indent))
881 })
882 .map_err(|e| format!("query symbols: {}", e))?;
883
884 let mut symbols: Vec<Symbol> = Vec::new();
885 let mut seen: HashSet<(String, String)> = HashSet::new();
889
890 let mut lines_cache: FxHashMap<String, Vec<String>> = FxHashMap::default();
893
894 for row in rows {
895 let (_phrase, file_path, line, block_id, end_line, indent) =
896 row.map_err(|e| format!("row: {}", e))?;
897
898 if !is_source_file(&file_path) {
900 continue;
901 }
902
903 if !lines_cache.contains_key(&file_path) {
905 let lines_vec: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&file_path) {
906 meta.lines.clone()
907 } else {
908 std::fs::read_to_string(&file_path)
909 .map(|c| c.lines().map(String::from).collect())
910 .unwrap_or_default()
911 };
912 lines_cache.insert(file_path.clone(), lines_vec);
913 }
914
915 let lines = &lines_cache[&file_path];
916
917 let signature = read_signature_line(lines, line);
919
920 if !is_definition_like(&signature) {
922 continue;
923 }
924
925 let real_name = extract_name_from_signature(&signature);
927 if real_name.len() < 3 {
928 continue;
929 }
930
931 if is_common_word(&real_name) {
933 continue;
934 }
935
936 if real_name.starts_with("__") && real_name.ends_with("__") {
938 continue;
939 }
940
941 let key = (real_name.clone(), file_path.clone());
943 if seen.contains(&key) {
944 continue;
945 }
946 seen.insert(key);
947
948 let doc_comment = read_doc_comment(lines, line);
950
951 symbols.push(Symbol {
952 name: real_name,
953 signature,
954 file: file_path,
955 line,
956 doc_comment,
957 block_id,
958 end_line,
959 indent,
960 });
961 }
962
963 Ok(symbols)
964}
965
966fn extract_name_from_signature(sig: &str) -> String {
971 let cleaned = sig.trim();
972
973 let mut rest = cleaned;
975 let keywords = [
976 "pub(crate) ", "pub(super) ", "pub ", "export ", "async ", "extern ",
977 "static ", "inline ", "virtual ", "override ", "final ", "abstract ",
978 "unsafe ", "mut ",
979 ];
980 loop {
981 let mut changed = false;
982 for kw in &keywords {
983 if rest.starts_with(kw) {
984 rest = &rest[kw.len()..];
985 changed = true;
986 break;
987 }
988 }
989 if !changed {
990 break;
991 }
992 }
993
994 let decl_kws = [
996 "fn ", "function ", "def ", "func ", "struct ", "class ", "enum ",
997 "trait ", "interface ", "type ", "impl ", "const ", "let ", "var ",
998 "val ",
999 ];
1000 for kw in &decl_kws {
1001 if rest.starts_with(kw) {
1002 rest = &rest[kw.len()..];
1003 break;
1004 }
1005 }
1006
1007 if rest.starts_with("impl ") {
1010 rest = &rest[5..];
1011 }
1012
1013 let end = rest
1015 .find(['(', '<', ':', '{', ' ', '=', ';'])
1016 .unwrap_or(rest.len());
1017 let name = rest[..end].trim();
1018
1019 let name = name.trim_start_matches('\'');
1021
1022 if name.is_empty() || name.len() < 2 {
1023 return String::new();
1024 }
1025
1026 if !name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
1028 return String::new();
1029 }
1030 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1031 return String::new();
1032 }
1033
1034 name.to_string()
1035}
1036
1037fn is_source_file(path: &str) -> bool {
1039 if path.contains("/bench/")
1042 || path.contains("/tests/")
1043 || path.contains("/test/")
1044 || path.contains("/target/")
1045 || path.contains("/vendor/")
1046 || path.contains("/node_modules/")
1047 || path.contains("/__pycache__/")
1048 || path.contains("/.git/")
1049 {
1050 return false;
1051 }
1052 let ext = Path::new(path)
1053 .extension()
1054 .and_then(|e| e.to_str())
1055 .unwrap_or("");
1056 matches!(
1057 ext,
1058 "rs" | "py" | "ts" | "tsx" | "js" | "jsx"
1059 | "go" | "java" | "c" | "h" | "cpp" | "cc" | "cxx"
1060 | "rb" | "swift" | "kt" | "scala" | "clj" | "ex" | "exs"
1061 | "elm" | "fs" | "ml" | "hs" | "lua" | "php" | "pl"
1062 )
1063}
1064
1065fn is_definition_like(line: &str) -> bool {
1069 let stripped = line.trim();
1070 if stripped.is_empty() {
1071 return false;
1072 }
1073
1074 if stripped.starts_with("//")
1076 || stripped.starts_with('#')
1077 || stripped.starts_with("/*")
1078 || stripped.starts_with("///")
1079 || stripped.starts_with("--")
1080 || stripped.starts_with('\'')
1081 {
1082 return false;
1083 }
1084
1085 if stripped.starts_with("if ")
1087 || stripped.starts_with("for ")
1088 || stripped.starts_with("while ")
1089 || stripped.starts_with("return ")
1090 || stripped.starts_with("match ")
1091 || stripped.starts_with("switch ")
1092 || stripped.starts_with("let ")
1093 || stripped.starts_with("let mut")
1094 || stripped.starts_with("const ")
1095 || stripped.starts_with("var ")
1096 || stripped.starts_with("static ") || stripped.starts_with("type ") || stripped.starts_with("enum ") || stripped.starts_with("f.write")
1100 || stripped.starts_with("println")
1101 || stripped.starts_with("print")
1102 || stripped.starts_with("eprintln")
1103 {
1104 return false;
1105 }
1106
1107 let check = stripped.trim_end_matches([';', ',']);
1108
1109 let ends_with_brace = check.ends_with('{')
1121 || (check.ends_with('}')
1122 && check.matches('{').count() == 1
1123 && check.matches('}').count() == 1);
1124 if check.contains('(') && ends_with_brace {
1125 let before_paren = check.split('(').next().unwrap_or("").trim();
1126 let mut name_part = before_paren;
1130 for kw in &["pub(crate) ", "pub(super) ", "pub ", "async ", "unsafe ",
1131 "extern ", "fn ", "function ", "func ", "export ",
1132 "static ", "inline ", "const ", "final ", "override "] {
1133 if name_part.starts_with(kw) {
1134 name_part = &name_part[kw.len()..];
1135 }
1136 }
1137 if !name_part.contains("::")
1140 && !name_part.contains('.')
1141 && !name_part.contains("->")
1142 && !name_part.contains("||")
1143 && !name_part.contains('=')
1144 && !name_part.contains(' ')
1145 && name_part.len() > 3
1146 {
1147 return true;
1148 }
1149 }
1150
1151 if check.starts_with("def ") && check.ends_with(':') {
1153 return true;
1154 }
1155
1156 if check.ends_with('{') {
1158 let lower = check.to_ascii_lowercase();
1159 if lower.contains(" struct ")
1160 || lower.contains(" class ")
1161 || lower.contains(" enum ")
1162 || lower.contains(" trait ")
1163 || lower.contains(" interface ")
1164 || lower.contains(" impl ")
1165 || lower.starts_with("struct ")
1166 || lower.starts_with("class ")
1167 || lower.starts_with("enum ")
1168 || lower.starts_with("trait ")
1169 || lower.starts_with("interface ")
1170 || lower.starts_with("impl ")
1171 || lower.starts_with("pub struct ")
1172 || lower.starts_with("pub enum ")
1173 || lower.starts_with("pub trait ")
1174 {
1175 return true;
1176 }
1177 }
1178
1179 if check.ends_with('{') && (check.starts_with("export interface ") || check.starts_with("interface ") || check.starts_with("export type ") || check.starts_with("type ")) {
1181 return true;
1182 }
1183
1184 false
1185}
1186
1187fn is_common_word(word: &str) -> bool {
1189 if word.len() < 3 {
1191 return true;
1192 }
1193 const COMMON: &[&str] = &[
1195 "the", "and", "for", "not", "are", "but", "you", "all", "can", "her",
1196 "was", "one", "our", "out", "has", "his", "how", "its", "may", "new",
1197 "now", "old", "see", "way", "who", "did", "get", "let", "say", "she",
1198 "too", "use", "any", "ask", "bad", "big", "day", "end", "few", "got",
1199 "had", "him", "his", "how", "its", "man", "men", "put", "run", "set",
1200 "try", "two", "use", "via", "yet", "also", "code", "data", "file",
1201 "from", "have", "into", "just", "like", "make", "more", "must",
1202 "only", "over", "some", "such", "than", "them", "then", "they",
1203 "this", "that", "when", "what", "where", "which", "while", "will",
1204 "with", "work", "would", "your", "about", "after", "again", "before",
1205 "being", "below", "could", "every", "first", "found", "great",
1206 "group", "having", "here", "high", "into", "itself", "just",
1207 "large", "last", "left", "like", "long", "made", "many", "most",
1208 "much", "never", "next", "once", "other", "part", "place", "right",
1209 "same", "should", "show", "since", "small", "some", "still",
1210 "such", "take", "their", "them", "then", "there", "these",
1211 "they", "thing", "think", "this", "those", "through", "time",
1212 "under", "until", "very", "well", "were", "what", "when",
1213 "where", "which", "while", "will", "with", "without", "would",
1214 "write", "your", "index", "free", "grammar", "instead", "tool",
1215 "tree", "language", "read", "work", "reliary",
1216 ];
1217 static COMMON_SET: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
1219 let set = COMMON_SET.get_or_init(|| std::collections::HashSet::from_iter(COMMON.iter().copied()));
1220 let lower = word.to_ascii_lowercase();
1221 set.contains(lower.as_str())
1222}
1223
1224fn read_signature_line(lines: &[String], line: i32) -> String {
1225 lines
1226 .get(line.max(0) as usize)
1227 .map(|s| s.trim().to_string())
1228 .unwrap_or_default()
1229}
1230
1231fn read_doc_comment(lines: &[String], def_line: i32) -> String {
1232 let def_idx = (def_line.max(0) as usize).min(lines.len().saturating_sub(1));
1233
1234 let mut doc_parts: Vec<String> = Vec::new();
1236 let mut j = def_idx.saturating_sub(1);
1237
1238 if j < lines.len() && lines[j].trim().ends_with("*/") {
1240 let mut block: Vec<String> = Vec::new();
1241 while j > 0 {
1242 let l = lines[j].trim();
1243 block.insert(0, l.to_string());
1244 if l.starts_with("/**") || l.starts_with("/*") {
1245 break;
1246 }
1247 j -= 1;
1248 }
1249 for bl in &block {
1250 let clean = bl
1251 .trim()
1252 .trim_start_matches("/**")
1253 .trim_start_matches("/*")
1254 .trim_end_matches("*/")
1255 .trim()
1256 .trim_start_matches('*')
1257 .trim();
1258 if !clean.is_empty() && !clean.starts_with('@') && clean != "/**" {
1259 doc_parts.insert(0, clean.to_string());
1260 }
1261 }
1262 }
1263
1264 if doc_parts.is_empty() {
1265 j = def_idx.saturating_sub(1);
1267 while j > 0 {
1268 let l = lines[j].trim();
1269 if let Some(rest) = l.strip_prefix("///") {
1270 doc_parts.insert(0, rest.trim().to_string());
1271 } else if let Some(rest) = l.strip_prefix("//") {
1272 doc_parts.insert(0, rest.trim().to_string());
1273 } else if l.starts_with('#') && !l.starts_with("#[") {
1274 doc_parts.insert(0, l[1..].trim().to_string());
1275 } else if l.is_empty() {
1276 } else {
1278 break;
1279 }
1280 j -= 1;
1281 }
1282 }
1283
1284 doc_parts.join(" ")
1285}
1286
1287fn is_noise_line(line: &str) -> bool {
1299 let trimmed = line.trim();
1300 if trimmed.is_empty() {
1301 return true;
1302 }
1303 if trimmed.len() < 15 {
1306 return true;
1307 }
1308 if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1310 return true;
1311 }
1312 if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1315 return true;
1316 }
1317 if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1319 return true;
1320 }
1321 if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1323 return true;
1324 }
1325 if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1327 return true;
1328 }
1329 if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1331 return true;
1332 }
1333 if trimmed.starts_with("assert") {
1335 return true;
1336 }
1337 if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1339 return true;
1340 }
1341 if (trimmed.starts_with("let ") || trimmed.starts_with("let mut "))
1345 && !trimmed.contains("(")
1346 && !trimmed.contains("{")
1347 && trimmed.ends_with(";")
1348 {
1349 return true;
1350 }
1351 let trimmed_for_return = if let Some(comment_start) = trimmed.find("//") {
1355 trimmed[..comment_start].trim()
1356 } else {
1357 trimmed
1358 };
1359 if let Some(rest) = trimmed_for_return.strip_prefix("return ") {
1360 if let Some(inner) = rest.strip_suffix(";") {
1363 let inner = inner.trim();
1364 if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1365 return true;
1367 }
1368 }
1369 if rest.contains(".lower()") || rest.contains(".upper()") {
1371 return true;
1372 }
1373 }
1374 if trimmed.starts_with("if isinstance(") {
1376 return true;
1377 }
1378 if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1380 return true;
1381 }
1382 if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1384 return true;
1385 }
1386 if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1388 return true;
1389 }
1390 if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1392 return true;
1393 }
1394 if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1396 return true;
1397 }
1398 if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1401 return true;
1402 }
1403 if let Some(rest) = trimmed.strip_prefix("self.") {
1405 if rest.starts_with("assert") || rest.contains(".assert") || rest.starts_with("setUp")
1406 || rest.starts_with("tearDown") || rest.starts_with("set_up")
1407 || rest.starts_with("tear_down") || rest.starts_with("skip(")
1408 || rest.starts_with("fixture") || rest.starts_with("patch(")
1409 {
1410 return true;
1411 }
1412 }
1413 if trimmed.contains(".append(") || trimmed.contains(".push(")
1416 || trimmed.contains(".insert(") || trimmed.contains(".remove(")
1417 || trimmed.contains(".clear(") || trimmed.contains(".drop(")
1418 || trimmed.contains(".len()") || trimmed.contains(".is_empty()")
1419 || trimmed.contains(".expect(") || trimmed.contains(".collect()")
1420 || trimmed.contains(".clone(") || trimmed.contains(".iter()")
1421 {
1422 return true;
1423 }
1424 if trimmed == "break;" || trimmed == "continue;"
1426 || trimmed == "break" || trimmed == "continue" {
1427 return true;
1428 }
1429 if trimmed.starts_with("from ") {
1431 return true;
1432 }
1433 if let Some(stripped_return) = trimmed.strip_prefix("return ") {
1435 let rest = stripped_return.trim().trim_end_matches(';').trim();
1436 if matches!(rest, "true" | "false" | "None" | "0" | "1" | "Self" | "-1") {
1437 return true;
1438 }
1439 }
1440 if trimmed.starts_with("log::") || trimmed.starts_with("tracing::")
1442 || trimmed.starts_with("debug!") || trimmed.starts_with("info!")
1443 || trimmed.starts_with("warn!") || trimmed.starts_with("error!")
1444 || trimmed.starts_with("print!") || trimmed.starts_with("eprint!")
1445 || trimmed.starts_with("println!") || trimmed.starts_with("eprintln!")
1446 || trimmed.starts_with("dbg!") || trimmed.starts_with("todo!")
1447 {
1448 return true;
1449 }
1450 if trimmed.starts_with("let _ =") || trimmed.starts_with("let _mut _ =")
1452 || trimmed.starts_with("let (_, _) =") || trimmed.starts_with("let (_, _, _) =")
1453 {
1454 return true;
1455 }
1456 let code_only = if let Some(comment_start) = trimmed.find("//") {
1459 trimmed[..comment_start].trim()
1460 } else {
1461 trimmed
1462 };
1463 if code_only.ends_with(".ok()?") || code_only.ends_with(".ok();")
1464 || code_only.contains(".expect(\"") || code_only.contains(".unwrap_or(")
1465 || code_only.contains(".unwrap_or_default(") || code_only.contains(".unwrap_or_else(")
1466 || (code_only.starts_with("return ") && code_only.contains(".ok()"))
1467 || (code_only.starts_with("return ") && code_only.contains(".err()"))
1468 || (code_only.starts_with("return ") && code_only.contains(".unwrap()"))
1469 {
1470 return true;
1471 }
1472 if let Some(eq_pos) = trimmed.find('=') {
1473 if eq_pos > 0 && !trimmed[eq_pos+1..].trim().starts_with("\"")
1474 && trimmed.contains(".to_string(")
1475 && !trimmed.contains("unsafe ") && !trimmed.contains("transmute")
1476 {
1477 return true;
1478 }
1479 }
1480 false
1481}
1482
1483
1484fn extract_surprise_from_body(
1490 body: &str,
1491 skeleton_freq: &FxHashMap<String, usize>,
1492) -> Vec<String> {
1493 let mut surprises: Vec<String> = Vec::new();
1494 let mut seen: HashSet<String> = HashSet::new();
1495
1496 let lines: Vec<&str> = body.lines().collect();
1497
1498 if let Some(m) = regex_capture(r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{", body) {
1505 add_surprise(&mut surprises, &mut seen,
1506 format!("auto-pass for input below {} chars (no gates applied)", m));
1507 }
1508
1509 if body.contains("expect_dash") || body.contains("j == 8") && body.contains("j == 13") && body.contains("j == 23") {
1512 add_surprise(&mut surprises, &mut seen,
1513 "UUID dash positions: 8, 13, 18, 23 (standard 8-4-4-4-12)".to_string());
1514 }
1515
1516 if body.contains("he - i") || body.contains("hex_len") {
1519 if let Some(m) = regex_capture(r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)", body) {
1520 add_surprise(&mut surprises, &mut seen,
1521 format!("hex-hash length range: {} to {}", m.split(',').next().unwrap_or(""), m.split(',').nth(1).unwrap_or("")));
1522 }
1523 }
1524
1525 if body.contains("5381") && body.contains("wrapping_mul") && body.contains("33") {
1528 add_surprise(&mut surprises, &mut seen,
1529 "djb3-33 hash: starts at 5381, multiplies by 33 (Bernstein variant)".to_string());
1530 }
1531
1532 if body.contains("bytes[") && body.contains("while") && !body.contains("chars()") {
1535 add_surprise(&mut surprises, &mut seen,
1536 "byte-indexed processing (not char-indexed)".to_string());
1537 }
1538
1539 if let Some(m) = regex_capture(r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)", body) {
1542 add_surprise(&mut surprises, &mut seen,
1543 format!("sentinel return: {} for empty input", m));
1544 }
1545
1546if body.contains("import") && body.contains("count") && body.contains('[') {
1549 add_surprise(&mut surprises, &mut seen,
1550 "imports collapsed into a single label '[N imports]'".to_string());
1551 }
1552
1553 if body.contains("flush") || (body.contains("if ") && body.contains("emit") && body.contains("reset")) {
1556 add_surprise(&mut surprises, &mut seen,
1557 "lazy flush: label emitted on next non-matching line, not immediately".to_string());
1558 }
1559
1560 if body.contains("Error") && body.contains("Comment") {
1563 let error_pos = body.find("Error").unwrap_or(0);
1564 let comment_pos = body.find("Comment").unwrap_or(0);
1565 if error_pos < comment_pos && error_pos > 0 && comment_pos > 0 {
1566 add_surprise(&mut surprises, &mut seen,
1567 "Error check runs BEFORE Comment check (priority order)".to_string());
1568 } else if comment_pos < error_pos && comment_pos > 0 && error_pos > 0 {
1569 add_surprise(&mut surprises, &mut seen,
1570 "Comment check runs BEFORE Error check (potential bug: errors in comments are missed)".to_string());
1571 }
1572 }
1573
1574 if regex_capture(r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+", body).is_some() {
1577 if body.contains("&&") {
1578 add_surprise(&mut surprises, &mut seen,
1579 "gates are AND-combined: ALL must fail for None return".to_string());
1580 } else if body.contains("||") {
1581 add_surprise(&mut surprises, &mut seen,
1582 "gates are OR-combined: ANY failing gate returns None".to_string());
1583 }
1584 }
1585
1586 if body.contains("aggressive") || body.contains("collapse") {
1589 if let Some(m) = regex_capture(r"\w+\s*>\s*\w+\s*\+\s*(\d+)", body) {
1590 add_surprise(&mut surprises, &mut seen,
1591 format!("minimum word length threshold: {} (below this, words kept verbatim)", m));
1592 }
1593 }
1594
1595 if let Some(m) = regex_capture(r"let\s+(\w+)\s*=\s*skeleton\(\w+\)", body) {
1598 add_surprise(&mut surprises, &mut seen,
1599 format!("operates on the SKELETON of input, not the original text (via {})", m));
1600 }
1601
1602 if body.contains("replace_all") && body.contains("&") {
1605 let placeholders: Vec<&str> = body
1606 .split(".replace_all")
1607 .skip(1)
1608 .filter_map(|s| {
1609 if let Some(idx) = s.find('"') {
1610 let after = &s[idx..];
1611 if let Some(end) = after[1..].find('"') {
1612 return Some(&after[1..=end]);
1613 }
1614 }
1615 None
1616 })
1617 .collect();
1618 if !placeholders.is_empty() {
1619 add_surprise(&mut surprises, &mut seen,
1620 format!("regex-based replacement: {}", placeholders.iter().take(3).cloned().collect::<Vec<_>>().join(", ")));
1621 }
1622 }
1623
1624if body.contains("num_end") && body.contains('.') {
1627 add_surprise(&mut surprises, &mut seen,
1628 "version detection: requires exactly three dot-separated numeric components (X.Y.Z)".to_string());
1629 }
1630
1631 let mut seen_nums: HashSet<String> = HashSet::new();
1633 let mut seen_conds: HashSet<String> = HashSet::new();
1634
1635 for (line_idx, line) in lines.iter().enumerate() {
1636 let trimmed = line.trim();
1637 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#")
1638 || trimmed.starts_with("///") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
1639 continue;
1640 }
1641 let code = trimmed;
1642
1643 for num in find_magic_numbers(code) {
1645 let key = format!("{}_{}", num, line_idx);
1646 if !seen_nums.contains(&key) {
1647 seen_nums.insert(key);
1648 let labeled = label_number(&num, code);
1649 if !labeled.starts_with("constant: 0") && !labeled.starts_with("constant: 1") {
1650 add_surprise(&mut surprises, &mut seen, labeled);
1651 }
1652 }
1653 }
1654
1655 if code.contains("if ") && (code.contains("&&") || code.contains("||")) {
1657 if let Some(cond) = extract_condition(code) {
1658 let cond_key: String = cond.chars().take(40).collect();
1659 if cond.len() > 10 && cond.len() < 200 && !seen_conds.contains(&cond_key) {
1660 seen_conds.insert(cond_key);
1661 add_surprise(&mut surprises, &mut seen,
1662 format!("compound condition: {}", cond));
1663 }
1664 }
1665 }
1666
1667 if line_idx < 15 && code.contains("if ") && code.contains("return") {
1669 if let Some(ret) = extract_return_value(code) {
1670 if !ret.is_empty() && ret != "true" && ret != "false" {
1671 add_surprise(&mut surprises, &mut seen,
1672 format!("early return → {}", ret));
1673 }
1674 }
1675 }
1676
1677 if code.contains(".wrapping_") {
1679 let ops: Vec<&str> = code
1680 .split(".wrapping_")
1681 .skip(1)
1682 .filter_map(|s| s.split('(').next())
1683 .collect();
1684 if !ops.is_empty() {
1685 add_surprise(&mut surprises, &mut seen,
1686 format!("wrapping: {}", ops.join(", ")));
1687 }
1688 }
1689 if code.contains("unwrap_or(") {
1690 if let Some(def) = extract_unwrap_default(code) {
1691 if !def.is_empty() {
1692 add_surprise(&mut surprises, &mut seen,
1693 format!("fallback: unwrap_or({})", def));
1694 }
1695 }
1696 }
1697
1698 if code.contains("with_capacity(") {
1700 if let Some(n) = extract_capacity(code) {
1701 add_surprise(&mut surprises, &mut seen,
1702 format!("capacity: {}", n));
1703 }
1704 }
1705
1706 if let Some(comment_start) = code.find("//") {
1708 let comment = code[comment_start + 2..].trim().to_ascii_lowercase();
1709 let comment_text = code[comment_start + 2..].trim();
1710 for sig in ["key", "difference", "note", "important", "surprise",
1711 "critical", "deliberate", "only fires", "not regex", "must"]
1712 {
1713 if comment.contains(sig) {
1714 add_surprise(&mut surprises, &mut seen,
1715 format!("source note: {}", comment_text));
1716 break;
1717 }
1718 }
1719 }
1720 }
1721
1722 for (line_idx, line) in lines.iter().enumerate() {
1724 if line_idx == 0 { continue; } let trimmed = line.trim();
1726 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
1727 continue;
1728 }
1729 if is_noise_line(trimmed) {
1730 continue;
1731 }
1732 let skel = aggressive_skeleton(trimmed);
1733 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 10 {
1734 continue;
1735 }
1736 let freq = skeleton_freq.get(&skel).copied().unwrap_or(0);
1737 if freq == 1 {
1738 let has_signal = trimmed.contains('(')
1743 || trimmed.contains("==") || trimmed.contains("!=")
1744 || trimmed.contains(" < ") || trimmed.contains(" > ")
1745 || trimmed.contains(">=") || trimmed.contains("<=")
1746 || trimmed.contains("&&") || trimmed.contains("||")
1747 || trimmed.contains("wrapping_") || trimmed.contains("unwrap_or")
1748 || trimmed.contains("match ") || trimmed.contains("=>");
1749 if has_signal {
1750 let snippet: String = trimmed.chars().take(80).collect();
1751 add_surprise(&mut surprises, &mut seen,
1752 format!("unique line: {}", snippet));
1753 }
1754 }
1755 }
1756
1757 let pre_dedup_len = surprises.len();
1760 let mut final_surprises: Vec<String> = Vec::new();
1761 let mut seen_fragments: HashSet<String> = HashSet::new();
1762 for s in surprises {
1763 let fragment: String = s
1765 .chars()
1766 .filter(|c| !c.is_whitespace())
1767 .take(40)
1768 .collect();
1769 if seen_fragments.contains(&fragment) {
1770 continue;
1771 }
1772 seen_fragments.insert(fragment);
1773 final_surprises.push(s);
1774 }
1775 let surprises = final_surprises;
1776 let post_dedup_len = surprises.len();
1777 if pre_dedup_len > post_dedup_len + 1 {
1778 }
1780 let pattern_count = surprises.iter()
1782 .filter(|s| !s.starts_with("rare pattern") && !s.starts_with("unique line"))
1783 .count();
1784 let freq_count = surprises.len() - pattern_count;
1785
1786 let limit = if pattern_count >= 3 {
1790 pattern_count.min(8) } else if pattern_count >= 1 {
1792 (pattern_count + freq_count.min(2)).min(8)
1793 } else {
1794 0
1796 };
1797
1798 surprises.into_iter().take(limit).collect()
1799}
1800
1801fn add_surprise(surprises: &mut Vec<String>, seen: &mut HashSet<String>, s: String) {
1802 let key: String = s.chars().take(60).collect();
1803 if !seen.contains(&key) {
1804 seen.insert(key);
1805 surprises.push(s);
1806 }
1807}
1808
1809fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1812 if pattern == r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{" {
1814 return manual_capture_len_guard(text);
1815 }
1816 if pattern == r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)" {
1817 return manual_capture_range(text);
1818 }
1819 if pattern == r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)" {
1820 return manual_capture_sentinel(text);
1821 }
1822 if pattern == r"\w+\s*>\s*\w+\s*\+\s*(\d+)" {
1823 return manual_capture_gt_plus(text);
1824 }
1825 if pattern == r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+" {
1826 return if text.contains("_threshold") { Some("found".to_string()) } else { None };
1827 }
1828 if pattern == r"let\s+(\w+)\s*=\s*skeleton\(\w+\)" {
1829 return manual_capture_skeleton_let(text);
1830 }
1831 None
1832}
1833
1834fn manual_capture_len_guard(text: &str) -> Option<String> {
1835 let bytes = text.as_bytes();
1836 let mut i = 0;
1837 while i < bytes.len() {
1838 if i + 2 < bytes.len() && bytes[i] == b'i' && bytes[i+1] == b'f' {
1839 i += 2;
1841 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1842 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1844 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; } }
1846 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1848 if i < bytes.len() && bytes[i] == b'<' { i += 1; }
1849 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1850 let start = i;
1852 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1853 if i > start {
1854 return Some(String::from_utf8_lossy(&bytes[start..i]).to_string());
1855 }
1856 }
1857 i += 1;
1858 }
1859 None
1860}
1861
1862fn manual_capture_range(text: &str) -> Option<String> {
1863 let bytes = text.as_bytes();
1864 let mut i = 0;
1865 while i < bytes.len() {
1866 if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i+1] == b'=' {
1867 i += 2;
1868 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1869 let start = i;
1870 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1871 if i > start {
1872 let first = String::from_utf8_lossy(&bytes[start..i]).to_string();
1873 while i < bytes.len() && bytes[i] != b'<' { i += 1; }
1875 if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i+1] == b'=' { i += 2; }
1876 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1877 let start2 = i;
1878 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1879 if i > start2 {
1880 let second = String::from_utf8_lossy(&bytes[start2..i]).to_string();
1881 return Some(format!("{},{}", first, second));
1882 }
1883 }
1884 }
1885 i += 1;
1886 }
1887 None
1888}
1889
1890fn manual_capture_sentinel(text: &str) -> Option<String> {
1891 if let Some(idx) = text.find("is_empty") {
1892 let after = &text[idx + 9..];
1893 if let Some(ret_idx) = after.find("return") {
1894 let after_ret = &after[ret_idx + 6..];
1895 let end = after_ret
1896 .find([';', '}', '\n'])
1897 .unwrap_or(after_ret.len());
1898 return Some(after_ret[..end].trim().to_string());
1899 }
1900 }
1901 None
1902}
1903
1904fn manual_capture_gt_plus(text: &str) -> Option<String> {
1905 if let Some(idx) = text.find('>') {
1906 let after = &text[idx + 1..];
1907 let trimmed = after.trim_start();
1909 let after_ident = trimmed
1911 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1912 let trimmed2 = after_ident.trim_start();
1913 if let Some(rest) = trimmed2.strip_prefix('+') {
1914 let trimmed3 = rest.trim_start();
1915 let end = trimmed3
1916 .find(|c: char| !c.is_ascii_digit())
1917 .unwrap_or(trimmed3.len());
1918 if end > 0 {
1919 return Some(trimmed3[..end].to_string());
1920 }
1921 }
1922 }
1923 None
1924}
1925
1926fn manual_capture_skeleton_let(text: &str) -> Option<String> {
1927 if let Some(idx) = text.find("let ") {
1928 let after = &text[idx + 4..];
1929 let after_ident = after
1930 .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1931 let trimmed = after_ident.trim_start();
1932 if let Some(after_eq) = trimmed.strip_prefix('=') {
1933 let after_eq = after_eq.trim_start();
1934 if let Some(sk_idx) = after_eq.find("skeleton(") {
1935 let before = &after_eq[..sk_idx].trim();
1936 if !before.is_empty() {
1937 return Some(before.to_string());
1938 }
1939 }
1940 }
1941 }
1942 None
1943}
1944
1945fn find_magic_numbers(code: &str) -> Vec<String> {
1946 let mut nums = Vec::new();
1947 let bytes = code.as_bytes();
1949 let mut i = 0;
1950 while i < bytes.len() {
1951 if (bytes[i] == b'>' || bytes[i] == b'<' || bytes[i] == b'=' || bytes[i] == b'!')
1952 && i + 1 < bytes.len()
1953 {
1954 i += 1;
1956 if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
1957 i += 1;
1958 }
1959 while i < bytes.len() && bytes[i] == b' ' {
1961 i += 1;
1962 }
1963 let start = i;
1965 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
1966 i += 1;
1967 }
1968 if i > start {
1969 let num = String::from_utf8_lossy(&bytes[start..i]).to_string();
1970 if num != "0" && num != "1" {
1971 nums.push(num);
1972 }
1973 }
1974 } else {
1975 i += 1;
1976 }
1977 }
1978 nums
1979}
1980
1981fn label_number(num: &str, code: &str) -> String {
1982 let lower = code.to_ascii_lowercase();
1983 if lower.contains("uuid") || (num == "8" || num == "13" || num == "18" || num == "23") {
1984 format!("UUID position: {}", num)
1985 } else if lower.contains("hash") || lower.contains("hex") {
1986 format!("hex-hash bound: {}", num)
1987 } else if lower.contains("entropy") || lower.contains("threshold") {
1988 format!("threshold: {}", num)
1989 } else if lower.contains("len") || lower.contains("length") || lower.contains("count") {
1990 format!("length: {}", num)
1991 } else if lower.contains("capacity") {
1992 format!("capacity: {}", num)
1993 } else {
1994 format!("constant: {}", num)
1995 }
1996}
1997
1998fn extract_condition(code: &str) -> Option<String> {
1999 let if_pos = code.find("if ")?;
2000 let after = &code[if_pos + 3..];
2001 let end = after
2003 .find(['{', '\n'])
2004 .unwrap_or(after.len());
2005 Some(after[..end].trim().to_string())
2006}
2007
2008fn extract_return_value(code: &str) -> Option<String> {
2009 let ret_pos = code.find("return")?;
2010 let after = &code[ret_pos + 6..];
2011 let end = after.find([';', '}']).unwrap_or(after.len());
2012 Some(after[..end].trim().to_string())
2013}
2014
2015fn extract_unwrap_default(code: &str) -> Option<String> {
2016 let pos = code.find("unwrap_or(")?;
2017 let after = &code[pos + 10..];
2018 let end = after.find(')')?;
2019 Some(after[..end].trim().to_string())
2020}
2021
2022fn extract_capacity(code: &str) -> Option<String> {
2023 let pos = code.find("with_capacity(")?;
2024 let after = &code[pos + 14..];
2025 let end = after.find(')')?;
2026 Some(after[..end].trim().to_string())
2027}
2028
2029fn build_cross_refs_from_index(
2039 db: &Connection,
2040 symbols: &[Symbol],
2041) -> FxHashMap<String, Vec<String>> {
2042 build_cross_refs_inner(db, symbols).unwrap_or_default()
2044}
2045
2046fn build_cross_refs_inner(
2047 db: &Connection,
2048 symbols: &[Symbol],
2049) -> Result<FxHashMap<String, Vec<String>>, rusqlite::Error> {
2050 let def_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2051 let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
2052 let mut ref_set: FxHashMap<String, HashSet<String>> = FxHashMap::default();
2053
2054 let mut all_pairs_stmt = db.prepare(
2059 "SELECT o.file_id, p.phrase, o.is_def
2060 FROM occurrence o
2061 JOIN phrases p ON o.phrase_id = p.id
2062 WHERE length(p.phrase) BETWEEN 3 AND 30
2063 ORDER BY o.file_id
2064 LIMIT 200000",
2065 )?;
2066
2067 let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
2069 for row in all_pairs_stmt
2070 .query_map([], |row| {
2071 Ok((
2072 row.get::<_, i64>(0)?,
2073 row.get::<_, String>(1)?,
2074 row.get::<_, i64>(2)? != 0,
2075 ))
2076 })?
2077 .flatten()
2078 {
2079 let (file_id, phrase, is_def) = row;
2080 file_to_phrases
2081 .entry(file_id)
2082 .or_default()
2083 .push((phrase, is_def));
2084 }
2085
2086 let mut phrase_to_non_def_files: FxHashMap<String, Vec<i64>> = FxHashMap::default();
2088 for (file_id, pairs) in &file_to_phrases {
2089 for (phrase, is_def) in pairs {
2090 if !*is_def {
2091 phrase_to_non_def_files
2092 .entry(phrase.clone())
2093 .or_default()
2094 .push(*file_id);
2095 }
2096 }
2097 }
2098
2099 for sym in symbols {
2101 let mut refs: HashSet<String> = HashSet::new();
2102
2103 let files_using_sym = match phrase_to_non_def_files.get(&sym.name) {
2104 Some(f) => f,
2105 None => continue,
2106 };
2107
2108 for &file_id in files_using_sym {
2109 if let Some(pairs) = file_to_phrases.get(&file_id) {
2110 for (phrase, is_def) in pairs {
2111 if *is_def
2112 && phrase != &sym.name
2113 && def_names.contains(phrase.as_str())
2114 && !is_common_word(phrase)
2115 && phrase.len() >= 3
2116 && phrase.len() < 30
2117 && !phrase.starts_with('_')
2118 && !phrase.ends_with("Error")
2119 && !phrase.ends_with("Result")
2120 && !phrase.ends_with("Option")
2121 && phrase != "Self"
2122 {
2123 refs.insert(phrase.clone());
2124 }
2125 }
2126 }
2127
2128 if refs.len() >= 5 {
2130 break;
2131 }
2132 }
2133
2134 if !refs.is_empty() {
2136 let mut sorted: Vec<String> = refs.into_iter().collect();
2137 sorted.sort();
2138 sorted.truncate(3);
2139 ref_set.insert(sym.name.clone(), sorted.into_iter().collect());
2140 }
2141 }
2142
2143 for (name, refs) in ref_set {
2145 let mut sorted: Vec<String> = refs.into_iter().collect();
2146 sorted.sort();
2147 sorted.truncate(3);
2148 cross_refs.insert(name, sorted);
2149 }
2150
2151 Ok(cross_refs)
2152}
2153
2154fn read_symbols_for_subset(
2162 selected: &[Symbol],
2163 _all_symbols: &[Symbol],
2164) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2165 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2166 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2167
2168 for sym in selected {
2172 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2173 meta.lines.to_vec()
2174 } else {
2175 std::fs::read_to_string(&sym.file)
2177 .map(|c| c.lines().map(String::from).collect())
2178 .unwrap_or_default()
2179 };
2180
2181 let start = (sym.line as usize).min(lines.len().saturating_sub(1));
2182 let end = find_body_end(lines.as_slice(), start, sym.indent).min(lines.len());
2183 if start < end {
2184 for line in &lines[start..end] {
2186 let trimmed = line.trim();
2187 if trimmed.is_empty() {
2188 continue;
2189 }
2190 let skel = aggressive_skeleton(trimmed);
2191 if !skel.is_empty() {
2192 *freq.entry(skel).or_insert(0) += 1;
2193 }
2194 }
2195 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2196 }
2197 }
2198
2199 (sources, freq)
2200}
2201
2202fn read_symbols_and_frequency(
2205 symbols: &[Symbol],
2206) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2207 let mut sources: FxHashMap<String, String> = FxHashMap::default();
2208 let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2209
2210 for sym in symbols {
2213 let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2214 meta.lines.to_vec()
2215 } else {
2216 std::fs::read_to_string(&sym.file)
2217 .map(|c| c.lines().map(String::from).collect())
2218 .unwrap_or_default()
2219 };
2220
2221 let start = (sym.line.max(0) as usize).min(lines.len().saturating_sub(1));
2226 let brace_end = find_body_end(lines.as_slice(), start, sym.indent);
2227 let index_end = (sym.end_line as usize).max(start + 1).min(lines.len());
2228 let end = brace_end.max(index_end).min(lines.len());
2229
2230 if start < end {
2231 for line in &lines[start..end] {
2233 let trimmed = line.trim();
2234 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
2235 continue;
2236 }
2237 let skel = aggressive_skeleton(trimmed);
2238 if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 5 {
2239 continue;
2240 }
2241 *freq.entry(skel).or_insert(0) += 1;
2242 }
2243
2244 sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2245 }
2246 }
2247
2248 (sources, freq)
2249}
2250
2251fn find_body_end(lines: &[String], start: usize, _base_indent: i32) -> usize {
2256 if start >= lines.len() {
2257 return start;
2258 }
2259 let first_line = &lines[start];
2260
2261 let stripped = first_line.trim();
2263 if stripped.starts_with("def ")
2264 || stripped.starts_with("class ")
2265 || stripped.starts_with("async def ")
2266 {
2267 let first_indent = first_line.len() - first_line.trim_start().len();
2268 for (i, line) in lines.iter().enumerate().skip(start + 1) {
2269 let trimmed = line.trim();
2270 if trimmed.is_empty() || trimmed.starts_with("#") {
2271 continue;
2272 }
2273 let indent = line.len() - line.trim_start().len();
2274 if indent <= first_indent
2276 && (trimmed.starts_with("def ")
2277 || trimmed.starts_with("class ")
2278 || trimmed.starts_with("async def "))
2279 {
2280 return i;
2281 }
2282 }
2283 return lines.len();
2284 }
2285
2286 let mut depth: i32 = 0;
2288 let mut seen_open = false;
2289 for (i, line) in lines.iter().enumerate().skip(start) {
2290 let trimmed = line.trim();
2291
2292 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
2294 continue;
2295 }
2296
2297 for ch in line.chars() {
2298 match ch {
2299 '{' => { depth += 1; seen_open = true; }
2300 '}' => { depth -= 1; }
2301 _ => {}
2302 }
2303 }
2304
2305 if seen_open && depth == 0 {
2306 return i + 1;
2307 }
2308 }
2309 lines.len()
2310}
2311
2312fn derive_crate_name(file_path: &str) -> String {
2319 let parts: Vec<&str> = file_path.split('/').collect();
2320 for (i, part) in parts.iter().enumerate() {
2321 if (*part == "crates" || *part == "packages") && i + 1 < parts.len() {
2322 return parts[i + 1].to_string();
2323 }
2324 }
2325 if parts.len() >= 2 {
2327 return parts[parts.len() - 2].to_string();
2328 }
2329 String::new()
2330}
2331
2332fn detect_modules(symbols: &[Symbol]) -> Vec<(String, Vec<Symbol>)> {
2333 let mut module_map: FxHashMap<String, Vec<Symbol>> = FxHashMap::default();
2334
2335 for sym in symbols {
2336 let module = derive_module_name(&sym.file);
2337 module_map.entry(module).or_default().push(sym.clone());
2338 }
2339
2340 let mut modules: Vec<(String, Vec<Symbol>)> = module_map.into_iter().collect();
2341 modules.sort_by(|a, b| a.0.cmp(&b.0));
2342 modules
2343}
2344
2345fn derive_module_name(file_path: &str) -> String {
2346 let path = Path::new(file_path);
2347 let components: Vec<_> = path.components().collect();
2348
2349 for (i, comp) in components.iter().enumerate() {
2351 let part = comp.as_os_str().to_string_lossy().to_string();
2352 if part == "packages" && i + 1 < components.len() {
2353 return format!("packages/{}", components[i + 1].as_os_str().to_string_lossy());
2354 }
2355 if part == "src" && i + 1 < components.len() {
2356 return format!("src/{}", components[i + 1].as_os_str().to_string_lossy());
2357 }
2358 if part == "crates" && i + 1 < components.len() {
2359 return format!("crates/{}", components[i + 1].as_os_str().to_string_lossy());
2360 }
2361 }
2362
2363 components
2365 .first()
2366 .map(|c| c.as_os_str().to_string_lossy().to_string())
2367 .unwrap_or_else(|| "root".to_string())
2368}
2369
2370fn build_cochange_line(_name: &str, signature: &str, _body: &str, cross_refs: &[String]) -> String {
2376 let lower = signature.to_ascii_lowercase();
2377 let mut parts: Vec<String> = Vec::new();
2378 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2379 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2380
2381 if is_enum {
2382 if !cross_refs.is_empty() {
2383 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2384 parts.push(format!("Add variant → update match sites: {}", sites.join(", ")));
2385 }
2386 } else if is_struct {
2387 if !cross_refs.is_empty() {
2388 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2389 parts.push(format!("Add field → update constructors: {}", sites.join(", ")));
2390 }
2391 } else {
2392 if !cross_refs.is_empty() {
2393 let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2394 parts.push(format!("Rename → update {} caller(s): {}", cross_refs.len(), sites.join(", ")));
2395 }
2396 }
2397 parts.join("; ")
2398}
2399
2400fn expand_type_signature(signature: &str, body: &str, def_line: i32) -> String {
2401 let sig_trimmed = signature.trim();
2402 let lower = sig_trimmed.to_ascii_lowercase();
2403 let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2404 let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2405 if !is_enum && !is_struct {
2406 return signature.to_string();
2407 }
2408 let mut names: Vec<String> = Vec::new();
2409 for (j, line) in body.lines().enumerate().skip(1) {
2410 let trimmed = line.trim();
2411 if trimmed == "}" || trimmed.is_empty() { continue; }
2412 if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with('*') {
2415 continue;
2416 }
2417 let abs_line = def_line + 1 + j as i32;
2421 if is_enum {
2422 let name = trimmed.split([',', '{', '(', ':']).next().unwrap_or("").trim().trim_end_matches(',');
2423 if !name.is_empty() && name.len() > 1 && name.chars().next().is_some_and(|c| c.is_uppercase() || c == '_') {
2424 names.push(format!("{} (line {})", name, abs_line));
2425 }
2426 } else if is_struct {
2427 if let Some(colon_pos) = trimmed.find(':') {
2428 let name = trimmed[..colon_pos].trim().trim_start_matches("pub ").trim();
2429 let ftype = trimmed[colon_pos + 1..]
2430 .trim()
2431 .trim_end_matches(',')
2432 .trim()
2433 .to_string();
2434 if !name.is_empty() && name.len() > 1 {
2435 if ftype.is_empty() || ftype == "{" {
2438 names.push(format!("{} (line {})", name, abs_line));
2439 } else {
2440 let ft = ftype.split('{').next().unwrap_or(&ftype).trim();
2441 names.push(format!("{}: {} (line {})", name, ft, abs_line));
2442 }
2443 }
2444 }
2445 }
2446 }
2447 if names.is_empty() { return signature.to_string(); }
2448 let kind = if is_enum { "variants" } else { "fields" };
2449 format!("{} // {}({})", sig_trimmed, kind, names.join(", "))
2450}
2451
2452fn render_entry(
2453 sym: &Symbol,
2454 body: &str,
2455 cross_refs: &[String],
2456 skeleton_freq: &FxHashMap<String, usize>,
2457 format: PackFormat,
2458) -> String {
2459 let crate_name = derive_crate_name(&sym.file);
2461 let header = if crate_name.is_empty() {
2462 format!("## {}", sym.name)
2463 } else {
2464 format!("## {}/{}", sym.name, crate_name)
2465 };
2466 let mut parts = vec![header];
2467
2468 if matches!(format, PackFormat::Full) && !sym.doc_comment.is_empty() {
2470 let l0 = sym
2471 .doc_comment
2472 .split('.')
2473 .next()
2474 .unwrap_or(&sym.doc_comment)
2475 .trim();
2476 let l0 = if l0.chars().count() > 120 {
2477 l0.chars().take(120).collect::<String>()
2479 } else {
2480 l0.to_string()
2481 };
2482 parts.push(format!("L0: {}", l0));
2483 }
2484
2485 let l2_line = expand_type_signature(&sym.signature, body, sym.line);
2487 parts.push(format!("L2: {} [{}:{}]", l2_line, sym.file, sym.line + 1));
2490
2491 if !body.is_empty() {
2493 let surprises = extract_surprise_from_body(body, skeleton_freq);
2494 if !surprises.is_empty() {
2495 parts.push(format!("L3: {}", surprises.join("; ")));
2496 }
2497 }
2498
2499 if matches!(format, PackFormat::Full) && cross_refs.len() >= 2 {
2501 parts.push(format!("Cross-refs: {}", cross_refs.iter().take(5).cloned().collect::<Vec<_>>().join(", ")));
2502 }
2503
2504 let l4 = build_cochange_line(&sym.name, &sym.signature, body, cross_refs);
2506 if !l4.is_empty() {
2507 parts.push(format!("L4: {}", l4));
2508 }
2509
2510 parts.push(String::new());
2511 parts.join("\n")
2512}
2513
2514#[cfg(test)]
2515mod noise_tests {
2516 use super::is_noise_line;
2517
2518 #[test]
2521 fn noise_classes_are_filtered() {
2522 let noise = [
2523 "",
2525 "}",
2526 "});",
2527 "};",
2528 "];",
2529 ")",
2530 "self.assertEqual(request.status, 200)",
2532 "self.assertIsNotNone(request.body)",
2533 "self.assertTrue(request.valid)",
2534 "block_phrases.append(request.text)",
2536 "results.append(item)",
2537 "log::info!(\"starting engine\")",
2539 "tracing::debug!(\"rpm = {}\", self.rpm)",
2540 "println!(\"done\")",
2541 "dbg!(value);",
2542 "#[derive(Debug, Clone, PartialEq)]",
2544 "derive(Serialize, Deserialize)",
2545 "break;",
2547 "continue;",
2548 "return None;",
2550 "return true;",
2551 "return false;",
2552 "let _ = do_thing();",
2554 "value.ok()?;",
2555 "thing.expect(\"must exist\");",
2556 "let x = y;",
2558 "let mut count = 0;",
2559 ];
2560 for line in noise {
2561 assert!(
2562 is_noise_line(line),
2563 "expected to be filtered as noise: {:?}",
2564 line
2565 );
2566 }
2567 }
2568
2569 #[test]
2571 fn signal_lines_are_preserved() {
2572 let signal = [
2573 "hash = hash.wrapping_mul(33).wrapping_add(byte);",
2574 "if total > threshold && !exhausted {",
2575 "match response_kind {",
2576 "let ratio = scored(matched, total) as f64;",
2577 "return Some(compute_fallback(input));",
2578 "buffer.extend_from_slice(&payload);",
2579 "channel.send(Message::Shutdown).await;",
2580 ];
2581 for line in signal {
2582 assert!(
2583 !is_noise_line(line),
2584 "expected to be preserved as signal: {:?}",
2585 line
2586 );
2587 }
2588 }
2589}