Skip to main content

reliary_pack/
lib.rs

1#![forbid(unsafe_code)]
2//! Holographic codebase pack generator.
3//!
4//! Produces a cache-stable, model-readable representation of a codebase
5//! that pre-loads enough information into an LLM's context to answer most
6//! codebase questions without tool calls.
7//!
8//! ## Format
9//!
10//! Each symbol gets one entry:
11//! ```text
12//! ## <symbol_name>
13//! L2: <signature>  [<file>:<line>]
14//! L3: <surprise facts only>
15//! ```
16//!
17//! L2 (signature) is always present — it anchors the model's prior.
18//! L3 (surprise) is present only when the function deviates from what
19//! a trained model would predict.
20//!
21//! ## Cross-references
22//!
23//! Uses the SQLite index (occurrence + block + scope_binding tables)
24//! for precise, disambiguated cross-references — not regex word matching.
25//!
26//! ## Surprise detection
27//!
28//! Uses skeleton-frequency analysis: lines whose aggressive_skeleton
29//! is rare across the codebase are "surprising"; lines whose skeleton
30//! is common are "expected" (the model's prior handles them).
31
32
33use std::collections::HashSet;
34use rustc_hash::FxHashMap;
35use std::path::Path;
36
37use rusqlite::Connection;
38
39use reliary_sift::classify::aggressive_skeleton;
40
41// ---------------------------------------------------------------------------
42// Public API
43// ---------------------------------------------------------------------------
44
45/// Pack format — controls which layers are included.
46#[derive(Debug, Clone, Copy)]
47pub enum PackFormat {
48    /// Signatures + surprise only (optimal, 85% reasoning accuracy).
49    L2L3,
50    /// Full: L0 (purpose) + L1 (paragraph) + L2 (signature) + L3 (surprise) + cross-refs.
51    Full,
52    /// Body snippets: signature + 5-8 representative body lines (simpler than L3).
53    /// The control experiment showed random body lines ≥ curated L3 surprise.
54    BodySnippets,
55}
56
57/// Gate decision returned by `should_inject_pack`.
58///
59/// Two-tier: high-complexity codebases get the full pack; middle-complexity
60/// ones get a minimal top-level map; low-complexity (or famous) ones get
61/// nothing. Calibration is based on measured pack-helpfulness data on
62/// reliary8 (HIGH), quale (MIDDLE), tokio (LOW).
63#[derive(Debug, Clone, Copy)]
64pub enum GateDecision {
65    /// Inject the full focused pack (~5K chars, hotspot-selected).
66    Full,
67    /// Inject a minimal top-level map only (~1K chars, signatures + L3 for top-15 symbols).
68    Minimal,
69    /// Skip the pack entirely — codebase is either too simple or already known by the model.
70    Skip,
71}
72
73/// Compute a composite complexity score for the codebase from the SQLite index.
74///
75/// `symbol_score = 0.4 * inbound_refs + 0.3 * outbound_calls + 0.3 * body_len_normalized`
76///
77/// The codebase score is the mean of symbol scores, weighted by inbound ref
78/// count (high-traffic symbols dominate). Higher = more complex = pack more
79/// useful.
80pub 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    // V54: delegate to the single-pass implementation.
90    compute_complexity_score_for(&db, path)
91}
92
93/// Decide whether to inject the pack based on the codebase's complexity score.
94///
95/// Two-tier gate:
96/// - score > HIGH_THRESHOLD: full pack
97/// - score > LOW_THRESHOLD: minimal map
98/// - else: skip
99///
100/// Thresholds calibrated against measured data:
101/// - reliary8 (complex, specific identifiers, tight coupling): score ≈ 6.4 → Full
102/// - quali (medium, simpler bodies): score ≈ 5.4 → Minimal
103/// - tokio (famous, similar score to quali): score ≈ 5.4 → Minimal
104///   (we can't distinguish famous from private via index alone —
105///   the model's training data exposure is external knowledge)
106pub 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
119/// Convenience: run the full gate pipeline for a codebase path.
120pub fn should_inject_pack(path: &str) -> Result<GateDecision, String> {
121    let score = compute_complexity_score(path)?;
122    Ok(gate_decision(score))
123}
124
125/// C1: Gate decision using an already-open connection (no double open).
126pub 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
131/// C12: Complexity score using an already-open connection.
132/// Combines 5 query_row calls into fewer queries via prepare_cached.
133pub fn compute_complexity_score_for(conn: &Connection, _path: &str) -> Result<f32, String> {
134    // V54: single pass over occurrence — one query computes symbol_count +
135    // non_def_count (source-only) instead of two separate full scans.
136    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    // Remaining scores: avg block span + specificity ratio (2 cheap queries).
161    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
185/// Generate a holographic pack for a codebase.
186///
187/// Reads the SQLite index at `<path>/.reliary/index.sqlite` and produces
188/// a markdown pack string.
189pub 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    // Verify the index is valid
198    reliary_search::schema::open_existing_db_safe(&db)
199        .map_err(|e| format!("index not initialized: {}", e))?;
200
201    // 1. Extract all definition symbols from the index
202    let symbols = extract_symbols_from_index(&db)?;
203
204    // 2. Read sources + build skeleton frequency in one pass (avoids re-reading files)
205    let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
206
207    // 3. Extract cross-references from the index
208    let cross_refs = build_cross_refs_from_index(&db, &symbols);
209
210    // 4. Render the pack
211    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
231/// Generate a hotspot-selected pack: top-K symbols by composite score.
232///
233/// Use this when queries are unknown — picks the most "interesting" symbols
234/// (most-called, most-connected, longest bodies) rather than the full pack.
235///
236/// Uses a LIGHT cross-ref computation (just inbound ref count per symbol)
237/// instead of the full cross-ref graph — this is 50x faster on large codebases.
238pub 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
247/// C1: Hotspot pack using an already-open connection.
248pub 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    // For hotspot mode: only read sources for the top-K symbols (after ranking)
257    // First pass: rank using just inbound ref count (no file I/O needed)
258    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) // body_len unknown yet; rank by inbound only
264        })
265        .collect();
266    // C33: select_nth_unstable_by for top-K (O(N) vs O(N log N)).
267    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    // Build cross-refs only for the selected symbols
281    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        // C4: Borrow instead of clone.
296        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
306/// Compute inbound reference counts for all symbols in one query.
307/// Only counts refs for symbols that pass the is_definition_like filter
308/// (otherwise common words like "std", "build", "fmt" dominate the ranking).
309fn 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    // Count: for each phrase that appears as a non-def occurrence, how many times?
317    // This is a proxy for "how many things reference this symbol".
318    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            // Only count if this phrase is a real symbol (passed is_definition_like)
346            if symbol_names.contains(phrase.as_str()) {
347                counts.insert(phrase, cnt as usize);
348            }
349        }
350    }
351
352    counts
353}
354
355/// Build cross-refs only for a subset of symbols (the selected hotspots).
356/// This is much faster than building the full cross-ref graph.
357fn 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    // BATCH: load all (file_id, phrase, is_def) in one query, then join in Rust.
366    // This avoids 27 × 2 SQL queries (each scanning the full occurrence table).
367    // V54: filter to known symbol names at the SQL level — cuts the load from
368    // the full occurrence table (OOM risk on large repos) to symbol-relevant
369    // rows only. The symbol set is small (thousands), occurrence rows for
370    // non-symbol phrases (common words, std lib) dominate the table.
371    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 each selected symbol: find files where it's used (non-def), then
401    // find other defined symbols in those files.
402    for sym in selected {
403        // Find files where this symbol is used (non-def)
404        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
438/// Auto-mode: gate + hotspot selection combined.
439///
440/// 1. Compute complexity score
441/// 2. Gate: Full → top-50 hotspot pack; Minimal → top-15; Skip → empty
442/// 3. Return the pack (or empty string)
443pub fn generate_pack_auto(path: &str) -> Result<String, String> {
444    // C1: Open DB once, pass through to gate + generate.
445    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
455/// C9: Extract repeated open_index logic.
456fn 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/// A pack entry parsed into searchable fields.
469#[derive(Debug, Clone)]
470pub struct PackEntry {
471    pub name: String,
472    pub header: String,   // "## name/crate"
473    pub body: String,     // L0+L2+L3+crossrefs text
474}
475
476/// Parse a full pack string into individual entries.
477pub 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            // Save previous entry
486            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    // Save last entry
503    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
513/// Tokenize text into lowercase word tokens for BM25 scoring.
514/// S1: Delegates to reliary_search::tokenize to use ASCII-only rules and
515/// match the main index's BM25 grammar (prevents CJK/Unicode fusion).
516fn tokenize(text: &str) -> Vec<String> {
517    reliary_search::tokenize(text)
518}
519
520/// Slice a full pack for a specific query using BM25 scoring.
521///
522/// Pre-build the full pack once, then call this per query to retrieve the
523/// top-K entries most relevant to the user's question. This handles the
524/// Per-query decision: should we slice the pack for this query, or skip it?
525///
526/// Based on N→F analysis from the 63-probe benchmark:
527/// - bug/crossref/detail queries: pack provides essential context (Δ = +1.0 to +1.8)
528/// - arch/impact/review queries: model already knows from training (Δ = +0.0)
529/// - discriminate/edge/what queries: marginal gain (Δ = +0.2 to +0.8)
530///
531/// This is a pure structural classifier — no API calls, no token cost.
532/// Runs in <1ms, zero side effects. Safe to call on every query.
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum SliceDecision {
535    /// Send the sliced pack — query benefits from context
536    Slice,
537    /// Skip the pack — model likely knows the answer
538    Skip,
539}
540
541/// Classify a query text to decide whether the sliced pack is worth sending.
542///
543/// Grammar-free: uses keyword presence and structural signals, not language parsing.
544pub fn should_slice_for_query(query: &str) -> SliceDecision {
545    let text = query.to_ascii_lowercase();
546
547    // SLICE: bug detection and cross-references (biggest deltas)
548    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    // SLICE: detail questions need specific values
558    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    // SLICE: question mentions specific symbol names that might not be in training.
564    // These are NAMES that the model is unlikely to know from generic Rust knowledge
565    // (project-specific internals), NOT common function names like "skeleton" or "classify".
566    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    // SKIP: architecture, impact, review, and what/edge/discriminate questions
579    // (model already knows from training, or marginal gain)
580    SliceDecision::Skip
581}
582
583/// Slice a pack for a specific query, but only if the classifier says it's worth it.
584/// Returns (sliced_content, decision) so callers can log/meter the decision.
585pub 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
593/// "which symbols to include" problem that hotspot selection can't solve.
594///
595/// Returns a sliced pack string with just the top-K entries.
596pub 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        // No query — return first top_k entries (fallback)
605        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    // Build a simple BM25 index over the pack entries
614    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        // Search over name + body (L0/L2/L3 text)
618        let combined = format!("{} {}", entry.name, entry.body);
619        doc_tokens.push(tokenize(&combined));
620    }
621
622    // Compute document frequencies for each query token
623    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    // Compute avg document length
634    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    // Score each document
642    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        // Count term frequencies in this doc
647        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        // Boost: if the entry name directly matches a query token, boost score
665        let name_lower = entry.name.to_ascii_lowercase();
666        for qt in &query_tokens {
667            if name_lower == *qt {
668                score += 5.0; // strong boost for exact name match
669            } else if name_lower.contains(qt.as_str()) {
670                score += 2.0; // weaker boost for substring match
671            }
672        }
673        scored.push((score, entry));
674    }
675
676    // Sort by score descending
677    scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
678
679    // Take top-K with score > 0
680    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    // Cross-ref expansion: for each selected entry, find cross-ref names
692    // in its body (Cross-refs: or L4: lines) and include those entries too.
693    // This ensures that when we slice for "detect_strategy", we also
694    // include its callers/callees like "detect_json", "detect_tabular".
695    let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
696    // Pre-build index for O(1) entry lookup by name
697    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                // L4: Rename → update N caller(s): name1, name2, name3
711                if let Some(pos) = line.find(": ") {
712                    let after = &line[pos + 2..];
713                    // Skip if it's "update N caller(s)" prefix — extract names after the last ":"
714                    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                    // O(1) lookup: exact match or prefix match
731                    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; // Only one Cross-refs/L4 line per entry
744            }
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
762/// Generate a hierarchical pack: top-level map + per-module packs.
763pub 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    // Group by module (top-level directory or packages/*)
782    let modules = detect_modules(&symbols);
783
784    // Build top-level map
785    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        // Build per-module pack
812        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// ---------------------------------------------------------------------------
838// Symbol extraction from the SQLite index
839// ---------------------------------------------------------------------------
840
841#[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
853/// Extract all definition symbols from the index.
854///
855/// Uses the `occurrence` table (is_def = 1) joined with `block` for body
856/// boundaries and `file_map` for file paths.
857fn 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    // Dedupe by (name, file_path) so the same name in different files
886    // (e.g. `skeleton` in reliary-output and reliary-sift) is kept,
887    // but the same name in the same file is deduped.
888    let mut seen: HashSet<(String, String)> = HashSet::new();
889
890    // S2: Use shared file_meta cache instead of per-call file_cache.
891    // file_meta::get returns Arc<FileMeta> with lines pre-split and cached.
892    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        // Skip non-source files first (markdown, config, etc.)
899        if !is_source_file(&file_path) {
900            continue;
901        }
902
903        // Ensure lines are cached — file_meta::get is the shared LRU cache.
904        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        // Read the source line for the signature
918        let signature = read_signature_line(lines, line);
919
920        // GRAMMAR-FREE DEFINITION FILTER:
921        if !is_definition_like(&signature) {
922            continue;
923        }
924
925        // Extract the REAL function name from the signature line
926        let real_name = extract_name_from_signature(&signature);
927        if real_name.len() < 3 {
928            continue;
929        }
930
931        // Skip common English words and noise
932        if is_common_word(&real_name) {
933            continue;
934        }
935
936        // Skip Python dunders (often auto-generated, not real symbols)
937        if real_name.starts_with("__") && real_name.ends_with("__") {
938            continue;
939        }
940
941        // Dedupe by (name, file) — keep same name in different files
942        let key = (real_name.clone(), file_path.clone());
943        if seen.contains(&key) {
944            continue;
945        }
946        seen.insert(key);
947
948        // Look backward for doc comments
949        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
966/// Extract the function/type name from a signature line (grammar-free).
967///
968/// Strips common keywords and type annotations, then takes the first
969/// identifier-like token that isn't a keyword or type.
970fn extract_name_from_signature(sig: &str) -> String {
971    let cleaned = sig.trim();
972
973    // Strip leading keywords (language-agnostic)
974    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    // Strip declaration keywords
995    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    // For impl blocks, skip the type name: "MaxwellGate {" → not a function
1008    // But "impl MaxwellGate {" → we want "MaxwellGate"
1009    if rest.starts_with("impl ") {
1010        rest = &rest[5..];
1011    }
1012
1013    // Find the first identifier-like token (up to (, <, :, {, space, =)
1014    let end = rest
1015        .find(['(', '<', ':', '{', ' ', '=', ';'])
1016        .unwrap_or(rest.len());
1017    let name = rest[..end].trim();
1018
1019    // Clean up: remove generics, lifetimes
1020    let name = name.trim_start_matches('\'');
1021
1022    if name.is_empty() || name.len() < 2 {
1023        return String::new();
1024    }
1025
1026    // Validate: must be identifier-like (alphanumeric + underscore, starts with alpha or _)
1027    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
1037/// Check if a file is a source code file (not markdown, config, etc.)
1038fn is_source_file(path: &str) -> bool {
1039    // Exclude common test/bench/vendor directories — these aren't the
1040    // codebase the user wants to explore.
1041    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
1065/// Grammar-free definition detection — same heuristic as the experiment.
1066/// Checks if a line looks like a function/struct/enum/class definition
1067/// without knowing the language.
1068fn is_definition_like(line: &str) -> bool {
1069    let stripped = line.trim();
1070    if stripped.is_empty() {
1071        return false;
1072    }
1073
1074    // Skip lines that are purely comments
1075    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    // Skip control flow (if/for/while/switch/match/return) — not definitions
1086    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 ")   // Rust statics, not function definitions
1097        || stripped.starts_with("type ")     // TypeScript type aliases
1098        || stripped.starts_with("enum ")     // already handles enum, but be explicit
1099        || 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    // Must contain a function-like or type-like declaration pattern.
1110    // We require BOTH an identifier AND a block-opener or type-marker.
1111
1112    // Function definition: has identifier( and ends with { or (
1113    // e.g., "pub fn skeleton(line: &str) -> String {"
1114    // e.g., "export function processSession(id: string): Result {"
1115    // Single-line body: ends with } but has balanced { ... } inside the line.
1116    //   e.g., "pub fn foo() -> i32 { 42 }"
1117    // BUT NOT: "std::iter::from_fn(move || {" (that's a call, not a definition)
1118    // BUT NOT: "search = run_altbackend_cli(" (that's an assignment, not a def)
1119    // BUT NOT: Python "def foo():" (handled separately below)
1120    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        // Must look like a function name: identifier optionally preceded by
1127        // keywords (pub, fn, def, function, etc.) but NOT a path (::) or a call.
1128        // Strip keywords to get the bare name
1129        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        // The name must be a simple identifier (no ::, no ., no ->, no =, no ||)
1138        // and must be > 3 chars to avoid noise like "new", "run", "get"
1139        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    // Python-style: def name(args):
1152    if check.starts_with("def ") && check.ends_with(':') {
1153        return true;
1154    }
1155
1156    // Class/struct/enum/interface/trait: ends with { and starts with keyword
1157    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    // TypeScript interface/type: ends with { or starts with interface/type
1180    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
1187/// Filter out common English words that the ingestion might mark as definitions.
1188fn is_common_word(word: &str) -> bool {
1189    // Skip very short words
1190    if word.len() < 3 {
1191        return true;
1192    }
1193    // Common English words that appear in comments/docs
1194    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    // P2-7: use HashSet for O(1) lookup instead of O(200) linear scan.
1218    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    // Look backward for doc comments (/// or /** */ or // or #)
1235    let mut doc_parts: Vec<String> = Vec::new();
1236    let mut j = def_idx.saturating_sub(1);
1237
1238    // Check for block comment ending right above
1239    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        // Line comments above
1266        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                // skip blanks between comment and definition
1277            } else {
1278                break;
1279            }
1280            j -= 1;
1281        }
1282    }
1283
1284    doc_parts.join(" ")
1285}
1286
1287// ---------------------------------------------------------------------------
1288// Skeleton-frequency for surprise detection
1289// ---------------------------------------------------------------------------
1290
1291/// Returns true if `line` is a non-surprising code pattern that should NOT
1292/// appear in L3 surprise output.
1293///
1294/// These are the lines that pollute the auto-generated L3 with noise — they
1295/// appear as "unique line: ..." entries but carry no real surprise value.
1296/// The hand-crafted pack omitted them entirely; this filter makes the auto-
1297/// generated pack match that judgment.
1298fn is_noise_line(line: &str) -> bool {
1299    let trimmed = line.trim();
1300    if trimmed.is_empty() {
1301        return true;
1302    }
1303    // Length: lines shorter than 15 chars are too short to carry surprise
1304    // (closing braces, semicolons, blank-ish content).
1305    if trimmed.len() < 15 {
1306        return true;
1307    }
1308    // Pure structural closers
1309    if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1310        return true;
1311    }
1312    // Imports / use statements (the skeleton normalizes these all to {w} anyway,
1313    // and the model's prior handles them).
1314    if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1315        return true;
1316    }
1317    // Comments — already excluded by tokenization, but double-check
1318    if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1319        return true;
1320    }
1321    // Python docstrings (multi-line string literals used as documentation)
1322    if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1323        return true;
1324    }
1325    // Python class-method internals — not surprising, just noise
1326    if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1327        return true;
1328    }
1329    // Python imports — model's prior handles these
1330    if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1331        return true;
1332    }
1333    // Python assertions — test helpers, not surprising
1334    if trimmed.starts_with("assert") {
1335        return true;
1336    }
1337    // Lines that are just a closing docstring/quote
1338    if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1339        return true;
1340    }
1341    // Simple variable bindings: `let x = y;` or `let mut x = ...;`
1342    // These are the most common "unique line" noise. Allow ones with parens
1343    // (`let x = foo(...)` may be a function call worth noting).
1344    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    // Simple return statements with no value or a bare identifier
1352    // (`return;` or `return x;` where x is a single word — not surprising)
1353    // S1 fix: strip inline comments first so trailing // doesn't bypass filters
1354    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        // `return;` or `return Some(x);` or `return None;` IS interesting if it's
1361        // a sentinel. Only filter the trivial `return <single_word>;` case.
1362        if let Some(inner) = rest.strip_suffix(";") {
1363            let inner = inner.trim();
1364            if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1365                // Single bare identifier return — likely noise
1366                return true;
1367            }
1368        }
1369        // Python returns: `return X.lower() in Y.lower()` — too common to be surprising
1370        if rest.contains(".lower()") || rest.contains(".upper()") {
1371            return true;
1372        }
1373    }
1374    // Python `if isinstance(obj, X):` — too common to be surprising
1375    if trimmed.starts_with("if isinstance(") {
1376        return true;
1377    }
1378    // Python `for X in Y:` — basic iteration, not surprising
1379    if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1380        return true;
1381    }
1382    // Python `self.X = Y` — attribute assignment, not surprising
1383    if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1384        return true;
1385    }
1386    // Simple assertions / panics with no context
1387    if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1388        return true;
1389    }
1390    // Attribute lines
1391    if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1392        return true;
1393    }
1394    // Mod / pub mod declarations
1395    if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1396        return true;
1397    }
1398    // S1 fix: derive attributes already filtered above with #include, but
1399    // additional patterns below were still producing "unique line:" noise
1400    if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1401        return true;
1402    }
1403    // S1 fix: Python test methods (self.assertX, self.setUp, self.X.method)
1404    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    // S1 fix: Rust method calls ending in ; — operations, not surprising
1414    //   e.g., `block_phrases.append(line);`
1415    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    // S1 fix: Rust single-token statements
1425    if trimmed == "break;" || trimmed == "continue;"
1426        || trimmed == "break" || trimmed == "continue" {
1427        return true;
1428    }
1429    // S1 fix: Python from X import statements (already covered by use)
1430    if trimmed.starts_with("from ") {
1431        return true;
1432    }
1433    // S1 fix: trivial returns (`return None;` `return true;` `return false;`)
1434    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    // S1 fix: log macros and trivial logging
1441    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    // S1 fix: ignore/discard assignments like `let _ = expr;`
1451    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    // S1 fix: option-result idioms (.ok() / .ok_or() patterns)
1457    // Strip trailing inline comment first (// GUARDED: ...)
1458    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
1484/// Extract surprise facts from a symbol's body using skeleton-frequency.
1485///
1486/// A line is "surprising" if its aggressive_skeleton appears < RARE_THRESHOLD
1487/// times across the codebase. We also extract magic numbers, compound
1488/// conditions, and early returns from surprising lines.
1489fn 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    // === Pass 1: Pattern-based detection ===
1499    // Recognize common "surprising code patterns" and generate labeled descriptions.
1500
1501    // Pattern: `<N>` auto-pass guard at the top of a function
1502    //   e.g., `if text.len() < 50 { return Some((1.0, text.len())); }`
1503    //   → "auto-pass for input below N chars (no gates applied)"
1504    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    // Pattern: UUID-like position checks (8, 13, 18, 23)
1510    //   e.g., `let expect_dash = j == 8 || j == 13 || j == 18 || j == 23;`
1511    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    // Pattern: hex-hash length range
1517    //   e.g., `he - i >= 7 && he - i <= 40`
1518    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    // Pattern: djb3-33 hash constants
1526    //   e.g., `let mut h: u64 = 5381; for b ... { h = h.wrapping_mul(33) }`
1527    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    // Pattern: byte-DFA processing
1533    //   e.g., iterating `bytes[i]` and `bytes[he]` directly
1534    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    // Pattern: sentinel return value
1540    //   e.g., `if s.is_empty() { return 0; }` or `return String::new()`
1541    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
1546// Pattern: import collapse label
1547    //   e.g., `format!("[{} imports]", import_count)`
1548    if 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    // Pattern: lazy/deferred flush
1554    //   e.g., only writing to result on the next non-matching line
1555    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    // Pattern: priority ordering
1561    //   e.g., Error check before Comment check
1562    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    // Pattern: gate combination (AND vs OR)
1575    //   e.g., `if ent < threshold && ratio > max { return None; }`
1576    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    // Pattern: minimum word length for aggressive collapsing
1587    //   e.g., `we > i + 1` (length > 1 keeps single letters verbatim)
1588    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    // Pattern: function called on the SKELETON, not the original
1596    //   e.g., `let s = skeleton(text); ... hash(s)`
1597    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    // Pattern: regex-based normalization (not byte-DFA)
1603    //   e.g., `PATTERNS.uuid.replace_all(&cleaned, "{uuid}")`
1604    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
1624// Pattern: three-comma version detection
1625    //   e.g., `X.Y.Z` with `num_end` calls
1626    if 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    // === Pass 2: Line-level signal extraction ===
1632    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        // Magic numbers in comparisons
1644        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        // Compound conditions (not already captured by patterns)
1656        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        // Early returns
1668        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        // Deliberate patterns
1678        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        // Capacity hints
1699        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        // Comment signals
1707        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    // === Pass 3: Skeleton-frequency rare patterns (tertiary) ===
1723    for (line_idx, line) in lines.iter().enumerate() {
1724        if line_idx == 0 { continue; } // skip signature line
1725        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            // S1 fix: only emit unique-line for lines with structural signal.
1739            // A truly unique line with no parens / braces / comparisons /
1740            // assignments is just noise (any random `let x = other_line;`
1741            // can be unique because of variable name).
1742            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    // S1 fix: dedupe across passes (early return + unique line often duplicate)
1758    // After both passes are done, dedupe entries that share key information.
1759    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        // Extract a canonical "fragment" — same early return + same unique line → same key
1764        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        // dedupe was meaningful; that's good
1779    }
1780    // Count pattern-based (non "rare pattern" / "unique line") vs. frequency-based
1781    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    // S1 fix: only include freq-based if we have 3+ pattern matches.
1787    // Without this guard, functions with zero patterns (just let-binding lines)
1788    // get 5 useless "unique line:" entries.
1789    let limit = if pattern_count >= 3 {
1790        pattern_count.min(8) // only pattern-based
1791    } else if pattern_count >= 1 {
1792        (pattern_count + freq_count.min(2)).min(8)
1793    } else {
1794        // No patterns matched — skip freq-based entirely (would be all noise)
1795        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
1809/// Minimal regex capture helper — finds the first match of `pattern` in `text`
1810/// and returns the first capture group as a String.
1811fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1812    // Avoid adding a regex dependency — simple manual patterns
1813    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            // skip "if"
1840            i += 2;
1841            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1842            // skip identifier
1843            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1844            // skip .len()
1845            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            // skip spaces and <
1847            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            // read number
1851            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                // find the second number after <=
1874                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        // skip spaces
1908        let trimmed = after.trim_start();
1909        // skip identifier
1910        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    // Simple regex-free approach: find numbers after comparison operators
1948    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            // Skip the operator
1955            i += 1;
1956            if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
1957                i += 1;
1958            }
1959            // Skip spaces
1960            while i < bytes.len() && bytes[i] == b' ' {
1961                i += 1;
1962            }
1963            // Read number
1964            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    // Find the end of condition (opening brace or end of line)
2002    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
2029// ---------------------------------------------------------------------------
2030// Cross-references from the SQLite index
2031// ---------------------------------------------------------------------------
2032
2033/// Build cross-reference map: symbol_name → list of symbols that reference it.
2034///
2035/// Uses file-level co-occurrence: find files where the symbol appears as a
2036/// non-def occurrence, then list other symbols defined in those files.
2037/// This is broader than block-matching and catches most real references.
2038fn build_cross_refs_from_index(
2039    db: &Connection,
2040    symbols: &[Symbol],
2041) -> FxHashMap<String, Vec<String>> {
2042    // C3: Return empty map on error instead of panicking.
2043    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    // === Phase 1: Batch-load ALL (file_id, phrase) pairs in one query ===
2055    // This avoids the N+1 query pattern that made the previous version O(N²).
2056    // V54: cap rows to bound memory on large repos (full occurrence table
2057    // can be 500K+ rows; symbol-relevant rows are a small subset).
2058    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    // For each file_id, the set of (phrase, is_def) pairs that appear in it
2068    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    // === Phase 2: For each symbol, find files where it's used (non-def) ===
2087    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    // Now for each symbol: look up files directly via inverted index
2100    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            // Don't bother scanning more files once we have enough refs
2129            if refs.len() >= 5 {
2130                break;
2131            }
2132        }
2133
2134        // Take top 3 refs, sorted
2135        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    // Convert sets to sorted vectors, capped at 3 to keep pack compact
2144    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
2154// ---------------------------------------------------------------------------
2155// Source reading
2156// ---------------------------------------------------------------------------
2157
2158/// Read sources for a SUBSET of symbols (the selected hotspots) + build
2159/// skeleton frequency from just those symbols' bodies. This is much faster
2160/// than reading all files — only ~50 file reads instead of ~700.
2161fn 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    // S2: use the shared file_meta cache instead of a per-call file_cache.
2169    // file_meta::get returns Arc<FileMeta> with lines pre-split and cached
2170    // across the MCP lifetime. Eliminates redundant disk reads.
2171    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            // Fallback: direct disk read for files not yet cached.
2176            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            // Iterate the slice directly — no join→re-split
2185            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
2202/// Read all symbol sources and build skeleton frequency in a single pass.
2203/// Avoids re-reading the same file multiple times across symbols.
2204fn 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    // S2: shared file_meta cache replaces per-call file_cache.
2211
2212    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        // Find the actual body extent. The block table's end_line is often wrong
2222        // (it undercounts for large functions), so we use brace-matching to find
2223        // the true end. We take the max of brace-matched end and end_line so
2224        // we never lose body content when the index is wrong.
2225        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            // Build skeleton frequency from body lines — iterate slice directly, no join→re-split
2232            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
2251/// Find the end of a function body by brace-matching from the start line.
2252/// Returns the line index AFTER the closing brace.
2253/// If brace-matching fails (e.g., Python uses indentation), falls back to
2254/// looking for the next def/class line at the same indent.
2255fn 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    // Python-style: find the next def/class at the same or lower indent
2262    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            // A new top-level def/class at the same indent marks the end
2275            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    // C-style: brace-matching
2287    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        // Skip comments and strings (crude — doesn't handle multi-line strings)
2293        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
2312// ---------------------------------------------------------------------------
2313// Module detection
2314// ---------------------------------------------------------------------------
2315
2316/// Extract the crate name from a file path.
2317/// Looks for `crates/<name>/` or `packages/<name>/` patterns.
2318fn 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    // Fallback: parent directory name
2326    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    // packages/<name>/... → packages/<name>
2350    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    // Fallback: first component
2364    components
2365        .first()
2366        .map(|c| c.as_os_str().to_string_lossy().to_string())
2367        .unwrap_or_else(|| "root".to_string())
2368}
2369
2370// ---------------------------------------------------------------------------
2371// Entry rendering
2372// ---------------------------------------------------------------------------
2373
2374/// Build an L4 co-change line for editing guidance.
2375fn 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        // Skip comments/attributes — a doc comment (`/// Tag: 0=...`) contains
2413        // a colon but is not a field declaration.
2414        if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with('*') {
2415            continue;
2416        }
2417        // body line j is 0-indexed within the body, whose first line is the
2418        // (0-indexed) definition line — so the 1-indexed source line is
2419        // def_line + 1 + j.
2420        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                    // Include the type and line so describe/pack answers
2436                    // field-typed and field-located questions in one call.
2437                    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    // Include crate/module name to disambiguate same-name symbols across crates
2460    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    // L0: one-line purpose (only in Full format, if doc comment exists)
2469    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            // Truncate at char boundary, not byte boundary
2478            l0.chars().take(120).collect::<String>()
2479        } else {
2480            l0.to_string()
2481        };
2482        parts.push(format!("L0: {}", l0));
2483    }
2484
2485    // L2: always present — expand enum/struct signatures to include variants/fields
2486    let l2_line = expand_type_signature(&sym.signature, body, sym.line);
2487    // sym.line is 0-indexed (read_signature_line indexes lines directly);
2488    // display 1-indexed to match every other tool surface.
2489    parts.push(format!("L2: {}  [{}:{}]", l2_line, sym.file, sym.line + 1));
2490
2491    // L3: surprise
2492    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    // Cross-refs (only in Full format, with ≥3 refs)
2500    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    // L4: co-change (editing guidance) — always present when we have cross-refs or type info
2505    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    /// Lines that must be filtered — each is a real noise class the S1 work
2519    /// targeted. If the filter is weakened these assertions fail.
2520    #[test]
2521    fn noise_classes_are_filtered() {
2522        let noise = [
2523            // blank / structural closers
2524            "",
2525            "}",
2526            "});",
2527            "};",
2528            "];",
2529            ")",
2530            // python assertions
2531            "self.assertEqual(request.status, 200)",
2532            "self.assertIsNotNone(request.body)",
2533            "self.assertTrue(request.valid)",
2534            // chained method calls of a collection
2535            "block_phrases.append(request.text)",
2536            "results.append(item)",
2537            // log macros
2538            "log::info!(\"starting engine\")",
2539            "tracing::debug!(\"rpm = {}\", self.rpm)",
2540            "println!(\"done\")",
2541            "dbg!(value);",
2542            // derive attributes
2543            "#[derive(Debug, Clone, PartialEq)]",
2544            "derive(Serialize, Deserialize)",
2545            // break / continue
2546            "break;",
2547            "continue;",
2548            // trivial returns
2549            "return None;",
2550            "return true;",
2551            "return false;",
2552            // discard bindings / option idioms
2553            "let _ = do_thing();",
2554            "value.ok()?;",
2555            "thing.expect(\"must exist\");",
2556            // simple bindings
2557            "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    /// Lines that must survive — filtering them would lose real signal.
2570    #[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}