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    // V80: symbols live in the `occurrence` table, which `trust` leaves empty
202    // (lazy index). Build it once so `reliary pack` on a freshly trusted repo
203    // returns symbols instead of "(0 symbols)". Idempotent.
204    let has_any: i64 = db
205        .query_row("SELECT EXISTS(SELECT 1 FROM occurrence LIMIT 1)", [], |r| r.get(0))
206        .unwrap_or(1);
207    if has_any == 0 {
208        let _ = reliary_search::lazy_occurrence::build_all_occurrence(&db);
209    }
210
211    // 1. Extract all definition symbols from the index
212    let symbols = extract_symbols_from_index(&db)?;
213
214    // 2. Read sources + build skeleton frequency in one pass (avoids re-reading files)
215    let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
216
217    // 3. Extract cross-references from the index
218    let cross_refs = build_cross_refs_from_index(&db, &symbols);
219
220    // 4. Render the pack
221    let mut entries = Vec::new();
222    entries.push(format!(
223        "# Holographic Pack — {} ({} symbols)\n",
224        Path::new(path)
225            .file_name()
226            .map(|n| n.to_string_lossy().to_string())
227            .unwrap_or_else(|| path.to_string()),
228        symbols.len()
229    ));
230
231    for sym in &symbols {
232        let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
233        let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
234        let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
235        entries.push(entry);
236    }
237
238    Ok(entries.join("\n"))
239}
240
241/// Generate a hotspot-selected pack: top-K symbols by composite score.
242///
243/// Use this when queries are unknown — picks the most "interesting" symbols
244/// (most-called, most-connected, longest bodies) rather than the full pack.
245///
246/// Uses a LIGHT cross-ref computation (just inbound ref count per symbol)
247/// instead of the full cross-ref graph — this is 50x faster on large codebases.
248pub fn generate_pack_hotspot(
249    path: &str,
250    format: PackFormat,
251    top_k: usize,
252) -> Result<String, String> {
253    let db = open_index(path)?;
254    generate_pack_hotspot_with(&db, path, format, top_k)
255}
256
257/// C1: Hotspot pack using an already-open connection.
258pub fn generate_pack_hotspot_with(
259    db: &Connection,
260    path: &str,
261    format: PackFormat,
262    top_k: usize,
263) -> Result<String, String> {
264    let symbols = extract_symbols_from_index(db)?;
265
266    // For hotspot mode: only read sources for the top-K symbols (after ranking)
267    // First pass: rank using just inbound ref count (no file I/O needed)
268    let inbound_counts = compute_inbound_ref_counts(db, &symbols);
269    let mut scored: Vec<(f32, &Symbol)> = symbols
270        .iter()
271        .map(|s| {
272            let inbound = *inbound_counts.get(&s.name).unwrap_or(&0) as f32;
273            (0.6 * inbound, s) // body_len unknown yet; rank by inbound only
274        })
275        .collect();
276    // C33: select_nth_unstable_by for top-K (O(N) vs O(N log N)).
277    let k = top_k.min(scored.len());
278    if k < scored.len() {
279        scored.select_nth_unstable_by(k, |a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
280    }
281    scored.truncate(k);
282
283    let selected: Vec<Symbol> = scored
284        .into_iter()
285        .map(|(_, s)| s.clone())
286        .collect();
287
288    let (symbol_sources, skeleton_freq) = read_symbols_for_subset(&selected, &symbols);
289
290    // Build cross-refs only for the selected symbols
291    let cross_refs = build_cross_refs_for_subset(db, &selected, &symbols);
292
293    let mut entries = Vec::with_capacity(selected.len() + 1);
294    entries.push(format!(
295        "# Holographic Pack — {} ({} of {} symbols, hotspot-ranked)\n",
296        Path::new(path)
297            .file_name()
298            .map(|n| n.to_string_lossy().to_string())
299            .unwrap_or_else(|| path.to_string()),
300        selected.len(),
301        symbols.len()
302    ));
303
304    for sym in &selected {
305        // C4: Borrow instead of clone.
306        let refs = cross_refs.get(&sym.name).map(|v| v.as_slice()).unwrap_or(&[]);
307        let source = symbol_sources.get(&sym.name).map(|s| s.as_str()).unwrap_or("");
308        let entry = render_entry(sym, source, refs, &skeleton_freq, format);
309        entries.push(entry);
310    }
311
312    let result = entries.join("\n");
313    Ok(result)
314}
315
316/// Compute inbound reference counts for all symbols in one query.
317/// Only counts refs for symbols that pass the is_definition_like filter
318/// (otherwise common words like "std", "build", "fmt" dominate the ranking).
319fn compute_inbound_ref_counts(
320    db: &Connection,
321    symbols: &[Symbol],
322) -> FxHashMap<String, usize> {
323    let mut counts: FxHashMap<String, usize> = FxHashMap::default();
324    let symbol_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
325
326    // Count: for each phrase that appears as a non-def occurrence, how many times?
327    // This is a proxy for "how many things reference this symbol".
328    let mut stmt = match db.prepare(
329        "SELECT p.phrase, COUNT(*) as cnt
330         FROM occurrence o
331         JOIN phrases p ON o.phrase_id = p.id
332         JOIN file_map f ON o.file_id = f.id
333         WHERE o.is_def = 0
334         AND f.file_path NOT LIKE '%/bench/%'
335         AND f.file_path NOT LIKE '%/tests/%'
336         AND f.file_path NOT LIKE '%/test/%'
337         AND f.file_path NOT LIKE '%/target/%'
338         AND f.file_path NOT LIKE '%/vendor/%'
339         AND f.file_path NOT LIKE '%/node_modules/%'
340         AND f.file_path NOT LIKE '%/__pycache__/%'
341         AND f.file_path NOT LIKE '%/.specstory/%'
342         GROUP BY p.phrase",
343    ) {
344        Ok(s) => s,
345        Err(_) => return counts,
346    };
347
348    let rows = stmt.query_map([], |row| {
349        Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
350    });
351
352    if let Ok(rows) = rows {
353        for row in rows.flatten() {
354            let (phrase, cnt) = row;
355            // Only count if this phrase is a real symbol (passed is_definition_like)
356            if symbol_names.contains(phrase.as_str()) {
357                counts.insert(phrase, cnt as usize);
358            }
359        }
360    }
361
362    counts
363}
364
365/// Build cross-refs only for a subset of symbols (the selected hotspots).
366/// This is much faster than building the full cross-ref graph.
367fn build_cross_refs_for_subset(
368    db: &Connection,
369    selected: &[Symbol],
370    all_symbols: &[Symbol],
371) -> FxHashMap<String, Vec<String>> {
372    let all_names: HashSet<&str> = all_symbols.iter().map(|s| s.name.as_str()).collect();
373    let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
374
375    // BATCH: load all (file_id, phrase, is_def) in one query, then join in Rust.
376    // This avoids 27 × 2 SQL queries (each scanning the full occurrence table).
377    // V54: filter to known symbol names at the SQL level — cuts the load from
378    // the full occurrence table (OOM risk on large repos) to symbol-relevant
379    // rows only. The symbol set is small (thousands), occurrence rows for
380    // non-symbol phrases (common words, std lib) dominate the table.
381    let mut all_pairs_stmt = match db.prepare(
382        "SELECT o.file_id, p.phrase, o.is_def
383         FROM occurrence o
384         JOIN phrases p ON o.phrase_id = p.id
385         WHERE p.phrase IN (SELECT phrase FROM phrases WHERE length(phrase) BETWEEN 3 AND 30)
386         ORDER BY o.file_id LIMIT 200000",
387    ) {
388        Ok(s) => s,
389        Err(_) => return cross_refs,
390    };
391
392    let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
393    for row in all_pairs_stmt
394        .query_map([], |row| {
395            Ok((
396                row.get::<_, i64>(0)?,
397                row.get::<_, String>(1)?,
398                row.get::<_, i64>(2)? != 0,
399            ))
400        })
401        .ok()
402        .into_iter()
403        .flatten()
404        .filter_map(|r| r.ok())
405    {
406        let (file_id, phrase, is_def) = row;
407        file_to_phrases.entry(file_id).or_default().push((phrase, is_def));
408    }
409
410    // For each selected symbol: find files where it's used (non-def), then
411    // find other defined symbols in those files.
412    for sym in selected {
413        // Find files where this symbol is used (non-def)
414        let usage_files: Vec<i64> = file_to_phrases
415            .iter()
416            .filter(|(_, pairs)| pairs.iter().any(|(p, is_def)| *p == sym.name && !*is_def))
417            .map(|(fid, _)| *fid)
418            .take(50)
419            .collect();
420
421        let mut refs: HashSet<String> = HashSet::new();
422        for file_id in usage_files {
423            if let Some(pairs) = file_to_phrases.get(&file_id) {
424                for (phrase, is_def) in pairs {
425                    if *is_def
426                        && *phrase != sym.name
427                        && all_names.contains(phrase.as_str())
428                        && !is_common_word(phrase)
429                    {
430                        refs.insert(phrase.clone());
431                    }
432                    if refs.len() >= 6 {
433                        break;
434                    }
435                }
436            }
437            if refs.len() >= 6 {
438                break;
439            }
440        }
441
442        cross_refs.insert(sym.name.clone(), refs.into_iter().take(6).collect());
443    }
444
445    cross_refs
446}
447
448/// Auto-mode: gate + hotspot selection combined.
449///
450/// 1. Compute complexity score
451/// 2. Gate: Full → top-50 hotspot pack; Minimal → top-15; Skip → empty
452/// 3. Return the pack (or empty string)
453pub fn generate_pack_auto(path: &str) -> Result<String, String> {
454    // C1: Open DB once, pass through to gate + generate.
455    let conn = open_index(path)?;
456    let decision = gate_decision_for(&conn, path)?;
457    
458    match decision {
459        GateDecision::Skip => Ok(String::new()),
460        GateDecision::Minimal => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 15),
461        GateDecision::Full => generate_pack_hotspot_with(&conn, path, PackFormat::L2L3, 50),
462    }
463}
464
465/// C9: Extract repeated open_index logic.
466fn open_index(codebase_path: &str) -> Result<rusqlite::Connection, String> {
467    let db_path = format!(
468        "{}/.reliary/index.sqlite",
469        codebase_path.trim_end_matches('/')
470    );
471    let conn = Connection::open(&db_path)
472        .map_err(|e| format!("pack: open {}: {}", db_path, e))?;
473    reliary_search::schema::open_existing_db_safe(&conn)
474        .map_err(|e| format!("index not initialized: {}", e))?;
475    Ok(conn)
476}
477
478/// A pack entry parsed into searchable fields.
479#[derive(Debug, Clone)]
480pub struct PackEntry {
481    pub name: String,
482    pub header: String,   // "## name/crate"
483    pub body: String,     // L0+L2+L3+crossrefs text
484}
485
486/// Parse a full pack string into individual entries.
487pub fn parse_pack_entries(pack: &str) -> Vec<PackEntry> {
488    let mut entries = Vec::new();
489    let mut current_name: Option<String> = None;
490    let mut current_header: String = String::new();
491    let mut current_body: String = String::new();
492
493    for line in pack.lines() {
494        if line.starts_with("## ") {
495            // Save previous entry
496            if let Some(name) = current_name.take() {
497                entries.push(PackEntry {
498                    name,
499                    header: std::mem::take(&mut current_header),
500                    body: std::mem::take(&mut current_body),
501                });
502            }
503            // Strip the `## ` header marker before taking the name; the old
504            // `strip_prefix("///")` never matched so the name was "## skeleton",
505            // which broke cross-ref expansion (keys never matched entry names).
506            let after = line.strip_prefix("## ").unwrap_or(line);
507            current_name = Some(after.split('/').next().unwrap_or("").trim().to_string());
508            current_header = line.to_string();
509        } else if current_name.is_some() {
510            if !current_body.is_empty() {
511                current_body.push('\n');
512            }
513            current_body.push_str(line);
514        }
515    }
516    // Save last entry
517    if let Some(name) = current_name {
518        entries.push(PackEntry {
519            name,
520            header: current_header,
521            body: current_body,
522        });
523    }
524    entries
525}
526
527/// Tokenize text into lowercase word tokens for BM25 scoring.
528/// S1: Delegates to reliary_search::tokenize to use ASCII-only rules and
529/// match the main index's BM25 grammar (prevents CJK/Unicode fusion).
530fn tokenize(text: &str) -> Vec<String> {
531    reliary_search::tokenize(text)
532}
533
534/// Slice a full pack for a specific query using BM25 scoring.
535///
536/// Pre-build the full pack once, then call this per query to retrieve the
537/// top-K entries most relevant to the user's question. This handles the
538/// Per-query decision: should we slice the pack for this query, or skip it?
539///
540/// Based on N→F analysis from the 63-probe benchmark:
541/// - bug/crossref/detail queries: pack provides essential context (Δ = +1.0 to +1.8)
542/// - arch/impact/review queries: model already knows from training (Δ = +0.0)
543/// - discriminate/edge/what queries: marginal gain (Δ = +0.2 to +0.8)
544///
545/// This is a pure structural classifier — no API calls, no token cost.
546/// Runs in <1ms, zero side effects. Safe to call on every query.
547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
548pub enum SliceDecision {
549    /// Send the sliced pack — query benefits from context
550    Slice,
551    /// Skip the pack — model likely knows the answer
552    Skip,
553}
554
555/// Classify a query text to decide whether the sliced pack is worth sending.
556///
557/// Grammar-free: uses keyword presence and structural signals, not language parsing.
558pub fn should_slice_for_query(query: &str) -> SliceDecision {
559    let text = query.to_ascii_lowercase();
560
561    // SLICE: bug detection and cross-references (biggest deltas)
562    if text.contains("bug") || text.contains("issue") || text.contains("wrong")
563        || text.contains("incorrect") || text.contains("missing") {
564        return SliceDecision::Slice;
565    }
566    if text.contains("call") || text.contains("caller") || text.contains("who uses")
567        || text.contains("references") || text.contains("called by") {
568        return SliceDecision::Slice;
569    }
570
571    // SLICE: detail questions need specific values
572    if text.contains("value") || text.contains("threshold") || text.contains("parameter")
573        || text.contains("default") || text.contains("exactly") || text.contains("what number") {
574        return SliceDecision::Slice;
575    }
576
577    // SLICE: question mentions specific symbol names that might not be in training.
578    // These are NAMES that the model is unlikely to know from generic Rust knowledge
579    // (project-specific internals), NOT common function names like "skeleton" or "classify".
580    let func_indicators = [
581        "find_clusters", "find_clusters_global", "skelhash", "skeleton_hash",
582        "aggressive_skeleton_hash", "maxwell", "maxwellgate", "should_drop",
583        "detect_strategy", "detect_tabular", "detect_json", "callgraph",
584        "is_definition_line", "reliary_sift", "reliary-output",
585    ];
586    for indicator in &func_indicators {
587        if text.contains(indicator) {
588            return SliceDecision::Slice;
589        }
590    }
591
592    // SKIP: architecture, impact, review, and what/edge/discriminate questions
593    // (model already knows from training, or marginal gain)
594    SliceDecision::Skip
595}
596
597/// Slice a pack for a specific query, but only if the classifier says it's worth it.
598/// Returns (sliced_content, decision) so callers can log/meter the decision.
599pub fn adaptive_slice(pack: &str, query: &str, top_k: usize) -> (String, SliceDecision) {
600    let decision = should_slice_for_query(query);
601    match decision {
602        SliceDecision::Slice => (slice_pack_for_query(pack, query, top_k), decision),
603        SliceDecision::Skip => (String::new(), decision),
604    }
605}
606
607/// "which symbols to include" problem that hotspot selection can't solve.
608///
609/// Returns a sliced pack string with just the top-K entries.
610pub fn slice_pack_for_query(pack: &str, query: &str, top_k: usize) -> String {
611    let entries = parse_pack_entries(pack);
612    if entries.is_empty() {
613        return String::new();
614    }
615
616    let query_tokens = tokenize(query);
617    if query_tokens.is_empty() {
618        // No query — return first top_k entries (fallback)
619        return entries
620            .into_iter()
621            .take(top_k)
622            .map(|e| format!("{}\n{}", e.header, e.body))
623            .collect::<Vec<_>>()
624            .join("\n\n");
625    }
626
627    // Build a simple BM25 index over the pack entries
628    let n_docs = entries.len() as f64;
629    let mut doc_tokens: Vec<Vec<String>> = Vec::with_capacity(entries.len());
630    for entry in &entries {
631        // Search over name + body (L0/L2/L3 text)
632        let combined = format!("{} {}", entry.name, entry.body);
633        doc_tokens.push(tokenize(&combined));
634    }
635
636    // Compute document frequencies for each query token
637    let mut df: FxHashMap<String, usize> = FxHashMap::default();
638    for tokens in &doc_tokens {
639        let unique: HashSet<&str> = tokens.iter().map(|s| s.as_str()).collect();
640        for t in &query_tokens {
641            if unique.contains(t.as_str()) {
642                *df.entry(t.clone()).or_insert(0) += 1;
643            }
644        }
645    }
646
647    // Compute avg document length
648    let total_len: usize = doc_tokens.iter().map(|d| d.len()).sum();
649    let avgdl = if n_docs > 0.0 {
650        total_len as f64 / n_docs
651    } else {
652        1.0
653    };
654
655    // Score each document
656    let mut scored: Vec<(f64, &PackEntry)> = Vec::with_capacity(entries.len());
657    for (i, entry) in entries.iter().enumerate() {
658        let doc_len = doc_tokens[i].len() as f64;
659        let mut score = 0.0;
660        // Count term frequencies in this doc
661        let mut tf: FxHashMap<&str, usize> = FxHashMap::default();
662        for t in &doc_tokens[i] {
663            *tf.entry(t.as_str()).or_insert(0) += 1;
664        }
665        for qt in &query_tokens {
666            let d_freq = *df.get(qt).unwrap_or(&0) as f64;
667            if d_freq == 0.0 {
668                continue;
669            }
670            let idf = ((n_docs - d_freq + 0.5) / (d_freq + 0.5) + 1.0).ln();
671            let term_freq = *tf.get(qt.as_str()).unwrap_or(&0) as f64;
672            if term_freq > 0.0 {
673                let bm25 = idf * (term_freq * (1.2 + 1.0))
674                    / (term_freq + 1.2 * (1.0 - 0.75 + 0.75 * (doc_len / avgdl)));
675                score += bm25;
676            }
677        }
678        // Boost: if the entry name directly matches a query token, boost score
679        let name_lower = entry.name.to_ascii_lowercase();
680        for qt in &query_tokens {
681            if name_lower == *qt {
682                score += 5.0; // strong boost for exact name match
683            } else if name_lower.contains(qt.as_str()) {
684                score += 2.0; // weaker boost for substring match
685            }
686        }
687        scored.push((score, entry));
688    }
689
690    // Sort by score descending
691    scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
692
693    // Take top-K with score > 0
694    let selected: Vec<&PackEntry> = scored
695        .into_iter()
696        .filter(|(s, _)| *s > 0.0)
697        .take(top_k)
698        .map(|(_, e)| e)
699        .collect();
700
701    if selected.is_empty() {
702        return String::new();
703    }
704
705    // Cross-ref expansion: for each selected entry, find cross-ref names
706    // in its body (Cross-refs: or L4: lines) and include those entries too.
707    // This ensures that when we slice for "detect_strategy", we also
708    // include its callers/callees like "detect_json", "detect_tabular".
709    let selected_names: HashSet<String> = selected.iter().map(|e| e.name.clone()).collect();
710    // Pre-build index for O(1) entry lookup by name
711    let mut entries_by_name: FxHashMap<&str, &PackEntry> = FxHashMap::default();
712    for e in &entries {
713        entries_by_name.entry(e.name.as_str()).or_insert(e);
714    }
715    let mut expanded: Vec<&PackEntry> = selected.clone();
716    let mut expanded_names: HashSet<String> = selected_names.clone();
717    let mut expansion_count = 0;
718    let max_expansion = 15usize.saturating_sub(top_k.min(15));
719    for entry in &selected {
720        for line in entry.body.lines() {
721            let refs_text = if let Some(rest) = line.strip_prefix("Cross-refs:") {
722                Some(rest.trim())
723            } else if line.starts_with("L4:") && line.contains("caller") {
724                // L4: Rename → update N caller(s): name1, name2, name3
725                if let Some(pos) = line.find(": ") {
726                    let after = &line[pos + 2..];
727                    // Skip if it's "update N caller(s)" prefix — extract names after the last ":"
728                    if let Some(last_colon) = after.rfind(": ") {
729                        Some(&after[last_colon + 2..])
730                    } else {
731                        Some(after)
732                    }
733                } else {
734                    None
735                }
736            } else {
737                None
738            };
739            if let Some(refs) = refs_text {
740                for ref_name in refs.split(',').take(3) {
741                    let ref_name = ref_name.trim();
742                    if ref_name.is_empty() { continue; }
743                    let ref_name_slash = format!("{}/", ref_name);
744                    // O(1) lookup: exact match or prefix match
745                    if let Some(e) = entries_by_name.get(ref_name).or_else(|| {
746                        entries_by_name.values().find(|e| e.name.starts_with(&ref_name_slash))
747                    }) {
748                        if !selected_names.contains(&e.name) && !expanded_names.contains(&e.name) {
749                            expanded_names.insert(e.name.clone());
750                            expanded.push(e);
751                            expansion_count += 1;
752                            if expansion_count >= max_expansion { break; }
753                        }
754                    }
755                    if expansion_count >= max_expansion { break; }
756                }
757                break; // Only one Cross-refs/L4 line per entry
758            }
759        }
760        if expansion_count >= max_expansion { break; }
761    }
762
763    let total_entries = expanded.len();
764
765    let mut result = format!(
766        "# Sliced Pack — {} entries for query: \"{}\"\n\n",
767        total_entries,
768        query.chars().take(80).collect::<String>()
769    );
770    for entry in &expanded {
771        result.push_str(&format!("{}\n{}\n\n", entry.header, entry.body));
772    }
773    result
774}
775
776/// Generate a hierarchical pack: top-level map + per-module packs.
777pub fn generate_hierarchical_pack(
778    path: &str,
779    format: PackFormat,
780    max_symbols_per_module: usize,
781) -> Result<HierarchicalPack, String> {
782    let db_path = format!(
783        "{}/.reliary/index.sqlite",
784        path.trim_end_matches('/')
785    );
786    let db = Connection::open(&db_path)
787        .map_err(|e| format!("cannot open index: {}", e))?;
788    reliary_search::schema::open_existing_db_safe(&db)
789        .map_err(|e| format!("index not initialized: {}", e))?;
790
791    let symbols = extract_symbols_from_index(&db)?;
792    let (symbol_sources, skeleton_freq) = read_symbols_and_frequency(&symbols);
793    let cross_refs = build_cross_refs_from_index(&db, &symbols);
794
795    // Group by module (top-level directory or packages/*)
796    let modules = detect_modules(&symbols);
797
798    // Build top-level map
799    let mut top_parts = vec![format!(
800        "# Holographic Pack — {} (top-level map)\n",
801        Path::new(path)
802            .file_name()
803            .map(|n| n.to_string_lossy().to_string())
804            .unwrap_or_else(|| path.to_string()),
805    )];
806
807    let mut module_packs: Vec<(String, String)> = Vec::new();
808
809    for (module_name, module_syms) in &modules {
810        let sym_count = module_syms.len();
811        let key_syms: Vec<String> = module_syms
812            .iter()
813            .filter(|s| !s.doc_comment.is_empty())
814            .take(5)
815            .map(|s| s.name.clone())
816            .collect();
817
818        top_parts.push(format!("## {}", module_name));
819        top_parts.push(format!("L0: {} symbols across this module", sym_count));
820        if !key_syms.is_empty() {
821            top_parts.push(format!("Key symbols: {}", key_syms.join(", ")));
822        }
823        top_parts.push(String::new());
824
825        // Build per-module pack
826        let limited: Vec<&Symbol> = module_syms.iter().take(max_symbols_per_module).collect();
827        if limited.is_empty() {
828            continue;
829        }
830        let mut pack_parts = vec![format!("# Holographic Pack — {}\n", module_name)];
831        for sym in limited {
832            let refs = cross_refs.get(&sym.name).cloned().unwrap_or_default();
833            let source = symbol_sources.get(&sym.name).cloned().unwrap_or_default();
834            let entry = render_entry(sym, &source, &refs, &skeleton_freq, format);
835            pack_parts.push(entry);
836        }
837        module_packs.push((module_name.clone(), pack_parts.join("\n")));
838    }
839
840    Ok(HierarchicalPack {
841        top_level: top_parts.join("\n"),
842        module_packs,
843    })
844}
845
846pub struct HierarchicalPack {
847    pub top_level: String,
848    pub module_packs: Vec<(String, String)>,
849}
850
851// ---------------------------------------------------------------------------
852// Symbol extraction from the SQLite index
853// ---------------------------------------------------------------------------
854
855#[derive(Debug, Clone)]
856pub struct Symbol {
857    pub name: String,
858    pub signature: String,
859    pub file: String,
860    pub line: i32,
861    pub doc_comment: String,
862    pub block_id: i64,
863    pub end_line: i32,
864    pub indent: i32,
865}
866
867/// Extract all definition symbols from the index.
868///
869/// Uses the `occurrence` table (is_def = 1) joined with `block` for body
870/// boundaries and `file_map` for file paths.
871fn extract_symbols_from_index(db: &Connection) -> Result<Vec<Symbol>, String> {
872    let mut stmt = db
873        .prepare(
874            "SELECT o.phrase_id, o.file_id, o.line, o.block_id,
875                    p.phrase, f.file_path,
876                    b.end_line, b.indent
877             FROM occurrence o
878             JOIN phrases p ON o.phrase_id = p.id
879             JOIN file_map f ON o.file_id = f.id
880             JOIN block b ON o.block_id = b.block_id
881             WHERE o.is_def = 1
882             ORDER BY f.file_path, o.line",
883        )
884        .map_err(|e| format!("query symbols: {}", e))?;
885
886    let rows = stmt
887        .query_map([], |row| {
888            let phrase: String = row.get(4)?;
889            let file_path: String = row.get(5)?;
890            let line: i32 = row.get(2)?;
891            let block_id: i64 = row.get(3)?;
892            let end_line: i32 = row.get(6)?;
893            let indent: i32 = row.get(7)?;
894            Ok((phrase, file_path, line, block_id, end_line, indent))
895        })
896        .map_err(|e| format!("query symbols: {}", e))?;
897
898    let mut symbols: Vec<Symbol> = Vec::new();
899    // Dedupe by (name, file_path) so the same name in different files
900    // (e.g. `skeleton` in reliary-output and reliary-sift) is kept,
901    // but the same name in the same file is deduped.
902    let mut seen: HashSet<(String, String)> = HashSet::new();
903
904    // S2: Use shared file_meta cache instead of per-call file_cache.
905    // file_meta::get returns Arc<FileMeta> with lines pre-split and cached.
906    let mut lines_cache: FxHashMap<String, Vec<String>> = FxHashMap::default();
907
908    for row in rows {
909        let (_phrase, file_path, line, block_id, end_line, indent) =
910            row.map_err(|e| format!("row: {}", e))?;
911
912        // Skip non-source files first (markdown, config, etc.)
913        if !is_source_file(&file_path) {
914            continue;
915        }
916
917        // Ensure lines are cached — file_meta::get is the shared LRU cache.
918        if !lines_cache.contains_key(&file_path) {
919            let lines_vec: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&file_path) {
920                meta.lines.clone()
921            } else {
922                std::fs::read_to_string(&file_path)
923                    .map(|c| c.lines().map(String::from).collect())
924                    .unwrap_or_default()
925            };
926            lines_cache.insert(file_path.clone(), lines_vec);
927        }
928
929        let lines = &lines_cache[&file_path];
930
931        // Read the source line for the signature
932        let signature = read_signature_line(lines, line);
933
934        // V80: use the SAME grammar-free classifier that ingest used to write
935        // the is_def=1 flag, instead of the pack's own Rust/JS/TS-shaped
936        // heuristic. The old `is_definition_like`/`extract_name_from_signature`
937        // pair only recognised Rust/JS/TS declarations, so a C#/Java/Kotlin
938        // repo packed 0 symbols and a mixed repo silently dropped its whole
939        // non-JS backend. The classifier handles every language the index does.
940        let ends_with_brace = signature.trim_end().ends_with('{');
941        let classified =
942            reliary_search::structural::classify_structural(&signature, 0, ends_with_brace, false);
943        let real_name = if classified.is_def {
944            classified.defined_name.map(|s| s.to_string()).unwrap_or_default()
945        } else if is_definition_like(&signature) {
946            // Fallback for a definition line the shared classifier does not
947            // flag (e.g. a syntax family it does not model) — keeps the old
948            // heuristic as a backstop so nothing regresses.
949            extract_name_from_signature(&signature)
950        } else {
951            String::new()
952        };
953        if real_name.len() < 3 {
954            continue;
955        }
956
957        // Skip common English words and noise
958        if is_common_word(&real_name) {
959            continue;
960        }
961
962        // Skip Python dunders (often auto-generated, not real symbols)
963        if real_name.starts_with("__") && real_name.ends_with("__") {
964            continue;
965        }
966
967        // Dedupe by (name, file) — keep same name in different files
968        let key = (real_name.clone(), file_path.clone());
969        if seen.contains(&key) {
970            continue;
971        }
972        seen.insert(key);
973
974        // Look backward for doc comments
975        let doc_comment = read_doc_comment(lines, line);
976
977        symbols.push(Symbol {
978            name: real_name,
979            signature,
980            file: file_path,
981            line,
982            doc_comment,
983            block_id,
984            end_line,
985            indent,
986        });
987    }
988
989    Ok(symbols)
990}
991
992/// Extract the function/type name from a signature line (grammar-free).
993///
994/// Strips common keywords and type annotations, then takes the first
995/// identifier-like token that isn't a keyword or type.
996fn extract_name_from_signature(sig: &str) -> String {
997    let cleaned = sig.trim();
998
999    // Strip leading keywords (language-agnostic)
1000    let mut rest = cleaned;
1001    let keywords = [
1002        "pub(crate) ", "pub(super) ", "pub ", "export ", "async ", "extern ",
1003        "static ", "inline ", "virtual ", "override ", "final ", "abstract ",
1004        "unsafe ", "mut ",
1005    ];
1006    loop {
1007        let mut changed = false;
1008        for kw in &keywords {
1009            if rest.starts_with(kw) {
1010                rest = &rest[kw.len()..];
1011                changed = true;
1012                break;
1013            }
1014        }
1015        if !changed {
1016            break;
1017        }
1018    }
1019
1020    // Strip declaration keywords
1021    let decl_kws = [
1022        "fn ", "function ", "def ", "func ", "struct ", "class ", "enum ",
1023        "trait ", "interface ", "type ", "impl ", "const ", "let ", "var ",
1024        "val ",
1025    ];
1026    for kw in &decl_kws {
1027        if rest.starts_with(kw) {
1028            rest = &rest[kw.len()..];
1029            break;
1030        }
1031    }
1032
1033    // For impl blocks, skip the type name: "MaxwellGate {" → not a function
1034    // But "impl MaxwellGate {" → we want "MaxwellGate"
1035    if rest.starts_with("impl ") {
1036        rest = &rest[5..];
1037    }
1038
1039    // Find the first identifier-like token (up to (, <, :, {, space, =)
1040    let end = rest
1041        .find(['(', '<', ':', '{', ' ', '=', ';'])
1042        .unwrap_or(rest.len());
1043    let name = rest[..end].trim();
1044
1045    // Clean up: remove generics, lifetimes
1046    let name = name.trim_start_matches('\'');
1047
1048    if name.is_empty() || name.len() < 2 {
1049        return String::new();
1050    }
1051
1052    // Validate: must be identifier-like (alphanumeric + underscore, starts with alpha or _)
1053    if !name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
1054        return String::new();
1055    }
1056    if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1057        return String::new();
1058    }
1059
1060    name.to_string()
1061}
1062
1063/// Check if a file is a source code file (not markdown, config, etc.)
1064fn is_source_file(path: &str) -> bool {
1065    // Exclude common test/bench/vendor directories — these aren't the
1066    // codebase the user wants to explore.
1067    if path.contains("/bench/")
1068        || path.contains("/tests/")
1069        || path.contains("/test/")
1070        || path.contains("/target/")
1071        || path.contains("/vendor/")
1072        || path.contains("/node_modules/")
1073        || path.contains("/__pycache__/")
1074        || path.contains("/.git/")
1075    {
1076        return false;
1077    }
1078    let ext = Path::new(path)
1079        .extension()
1080        .and_then(|e| e.to_str())
1081        .unwrap_or("");
1082    // V80: this list is the ONLY gate before a file's symbols can enter the
1083    // pack. It omitted `cs`, so a .NET repo packed 0 symbols and a mixed repo
1084    // silently dropped its entire C# backend (no warning). It also omitted the
1085    // other common source languages the index already handles. Grammar-free:
1086    // a citation-shape extension list, mirroring the verify extractor.
1087    matches!(
1088        ext,
1089        "rs" | "py" | "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs"
1090            | "go" | "java" | "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hh"
1091            | "rb" | "swift" | "kt" | "kts" | "scala" | "clj" | "ex" | "exs"
1092            | "elm" | "fs" | "fsx" | "ml" | "mli" | "hs" | "lua" | "php" | "pl"
1093            | "cs" | "dart" | "groovy" | "m" | "mm" | "jl" | "r" | "v"
1094    )
1095}
1096
1097/// Grammar-free definition detection — same heuristic as the experiment.
1098/// Checks if a line looks like a function/struct/enum/class definition
1099/// without knowing the language.
1100fn is_definition_like(line: &str) -> bool {
1101    let stripped = line.trim();
1102    if stripped.is_empty() {
1103        return false;
1104    }
1105
1106    // Skip lines that are purely comments
1107    if stripped.starts_with("//")
1108        || stripped.starts_with('#')
1109        || stripped.starts_with("/*")
1110        || stripped.starts_with("///")
1111        || stripped.starts_with("--")
1112        || stripped.starts_with('\'')
1113    {
1114        return false;
1115    }
1116
1117    // Skip control flow (if/for/while/switch/match/return) — not definitions
1118    if stripped.starts_with("if ")
1119        || stripped.starts_with("for ")
1120        || stripped.starts_with("while ")
1121        || stripped.starts_with("return ")
1122        || stripped.starts_with("match ")
1123        || stripped.starts_with("switch ")
1124        || stripped.starts_with("let ")
1125        || stripped.starts_with("let mut")
1126        || stripped.starts_with("const ")
1127        || stripped.starts_with("var ")
1128        || stripped.starts_with("static ")   // Rust statics, not function definitions
1129        || stripped.starts_with("type ")     // TypeScript type aliases
1130        || stripped.starts_with("enum ")     // already handles enum, but be explicit
1131        || stripped.starts_with("f.write")
1132        || stripped.starts_with("println")
1133        || stripped.starts_with("print")
1134        || stripped.starts_with("eprintln")
1135    {
1136        return false;
1137    }
1138
1139    let check = stripped.trim_end_matches([';', ',']);
1140
1141    // Must contain a function-like or type-like declaration pattern.
1142    // We require BOTH an identifier AND a block-opener or type-marker.
1143
1144    // Function definition: has identifier( and ends with { or (
1145    // e.g., "pub fn skeleton(line: &str) -> String {"
1146    // e.g., "export function processSession(id: string): Result {"
1147    // Single-line body: ends with } but has balanced { ... } inside the line.
1148    //   e.g., "pub fn foo() -> i32 { 42 }"
1149    // BUT NOT: "std::iter::from_fn(move || {" (that's a call, not a definition)
1150    // BUT NOT: "search = run_altbackend_cli(" (that's an assignment, not a def)
1151    // BUT NOT: Python "def foo():" (handled separately below)
1152    let ends_with_brace = check.ends_with('{')
1153        || (check.ends_with('}')
1154            && check.matches('{').count() == 1
1155            && check.matches('}').count() == 1);
1156    if check.contains('(') && ends_with_brace {
1157        let before_paren = check.split('(').next().unwrap_or("").trim();
1158        // Must look like a function name: identifier optionally preceded by
1159        // keywords (pub, fn, def, function, etc.) but NOT a path (::) or a call.
1160        // Strip keywords to get the bare name
1161        let mut name_part = before_paren;
1162        for kw in &["pub(crate) ", "pub(super) ", "pub ", "async ", "unsafe ",
1163                    "extern ", "fn ", "function ", "func ", "export ",
1164                    "static ", "inline ", "const ", "final ", "override "] {
1165            if name_part.starts_with(kw) {
1166                name_part = &name_part[kw.len()..];
1167            }
1168        }
1169        // The name must be a simple identifier (no ::, no ., no ->, no =, no ||)
1170        // and must be > 3 chars to avoid noise like "new", "run", "get"
1171        if !name_part.contains("::")
1172            && !name_part.contains('.')
1173            && !name_part.contains("->")
1174            && !name_part.contains("||")
1175            && !name_part.contains('=')
1176            && !name_part.contains(' ')
1177            && name_part.len() > 3
1178        {
1179            return true;
1180        }
1181    }
1182
1183    // Python-style: def name(args):
1184    if check.starts_with("def ") && check.ends_with(':') {
1185        return true;
1186    }
1187
1188    // Class/struct/enum/interface/trait: ends with { and starts with keyword
1189    if check.ends_with('{') {
1190        let lower = check.to_ascii_lowercase();
1191        if lower.contains(" struct ")
1192            || lower.contains(" class ")
1193            || lower.contains(" enum ")
1194            || lower.contains(" trait ")
1195            || lower.contains(" interface ")
1196            || lower.contains(" impl ")
1197            || lower.starts_with("struct ")
1198            || lower.starts_with("class ")
1199            || lower.starts_with("enum ")
1200            || lower.starts_with("trait ")
1201            || lower.starts_with("interface ")
1202            || lower.starts_with("impl ")
1203            || lower.starts_with("pub struct ")
1204            || lower.starts_with("pub enum ")
1205            || lower.starts_with("pub trait ")
1206        {
1207            return true;
1208        }
1209    }
1210
1211    // TypeScript interface/type: ends with { or starts with interface/type
1212    if check.ends_with('{') && (check.starts_with("export interface ") || check.starts_with("interface ") || check.starts_with("export type ") || check.starts_with("type ")) {
1213        return true;
1214    }
1215
1216    false
1217}
1218
1219/// Filter out common English words that the ingestion might mark as definitions.
1220fn is_common_word(word: &str) -> bool {
1221    // Skip very short words
1222    if word.len() < 3 {
1223        return true;
1224    }
1225    // Common English words that appear in comments/docs
1226    const COMMON: &[&str] = &[
1227        "the", "and", "for", "not", "are", "but", "you", "all", "can", "her",
1228        "was", "one", "our", "out", "has", "his", "how", "its", "may", "new",
1229        "now", "old", "see", "way", "who", "did", "get", "let", "say", "she",
1230        "too", "use", "any", "ask", "bad", "big", "day", "end", "few", "got",
1231        "had", "him", "his", "how", "its", "man", "men", "put", "run", "set",
1232        "try", "two", "use", "via", "yet", "also", "code", "data", "file",
1233        "from", "have", "into", "just", "like", "make", "more", "must",
1234        "only", "over", "some", "such", "than", "them", "then", "they",
1235        "this", "that", "when", "what", "where", "which", "while", "will",
1236        "with", "work", "would", "your", "about", "after", "again", "before",
1237        "being", "below", "could", "every", "first", "found", "great",
1238        "group", "having", "here", "high", "into", "itself", "just",
1239        "large", "last", "left", "like", "long", "made", "many", "most",
1240        "much", "never", "next", "once", "other", "part", "place", "right",
1241        "same", "should", "show", "since", "small", "some", "still",
1242        "such", "take", "their", "them", "then", "there", "these",
1243        "they", "thing", "think", "this", "those", "through", "time",
1244        "under", "until", "very", "well", "were", "what", "when",
1245        "where", "which", "while", "will", "with", "without", "would",
1246        "write", "your", "index", "free", "grammar", "instead", "tool",
1247        "tree", "language", "read", "work", "reliary",
1248    ];
1249    // P2-7: use HashSet for O(1) lookup instead of O(200) linear scan.
1250    static COMMON_SET: std::sync::OnceLock<std::collections::HashSet<&'static str>> = std::sync::OnceLock::new();
1251    let set = COMMON_SET.get_or_init(|| std::collections::HashSet::from_iter(COMMON.iter().copied()));
1252    let lower = word.to_ascii_lowercase();
1253    set.contains(lower.as_str())
1254}
1255
1256fn read_signature_line(lines: &[String], line: i32) -> String {
1257    lines
1258        .get(line.max(0) as usize)
1259        .map(|s| s.trim().to_string())
1260        .unwrap_or_default()
1261}
1262
1263fn read_doc_comment(lines: &[String], def_line: i32) -> String {
1264    let def_idx = (def_line.max(0) as usize).min(lines.len().saturating_sub(1));
1265
1266    // Look backward for doc comments (/// or /** */ or // or #)
1267    let mut doc_parts: Vec<String> = Vec::new();
1268    let mut j = def_idx.saturating_sub(1);
1269
1270    // Check for block comment ending right above
1271    if j < lines.len() && lines[j].trim().ends_with("*/") {
1272        let mut block: Vec<String> = Vec::new();
1273        while j > 0 {
1274            let l = lines[j].trim();
1275            block.insert(0, l.to_string());
1276            if l.starts_with("/**") || l.starts_with("/*") {
1277                break;
1278            }
1279            j -= 1;
1280        }
1281        for bl in &block {
1282            let clean = bl
1283                .trim()
1284                .trim_start_matches("/**")
1285                .trim_start_matches("/*")
1286                .trim_end_matches("*/")
1287                .trim()
1288                .trim_start_matches('*')
1289                .trim();
1290            if !clean.is_empty() && !clean.starts_with('@') && clean != "/**" {
1291                doc_parts.insert(0, clean.to_string());
1292            }
1293        }
1294    }
1295
1296    if doc_parts.is_empty() {
1297        // Line comments above
1298        j = def_idx.saturating_sub(1);
1299        while j > 0 {
1300            let l = lines[j].trim();
1301            if let Some(rest) = l.strip_prefix("///") {
1302                doc_parts.insert(0, rest.trim().to_string());
1303            } else if let Some(rest) = l.strip_prefix("//") {
1304                doc_parts.insert(0, rest.trim().to_string());
1305            } else if l.starts_with('#') && !l.starts_with("#[") {
1306                doc_parts.insert(0, l[1..].trim().to_string());
1307            } else if l.is_empty() {
1308                // skip blanks between comment and definition
1309            } else {
1310                break;
1311            }
1312            j -= 1;
1313        }
1314    }
1315
1316    doc_parts.join(" ")
1317}
1318
1319// ---------------------------------------------------------------------------
1320// Skeleton-frequency for surprise detection
1321// ---------------------------------------------------------------------------
1322
1323/// Returns true if `line` is a non-surprising code pattern that should NOT
1324/// appear in L3 surprise output.
1325///
1326/// These are the lines that pollute the auto-generated L3 with noise — they
1327/// appear as "unique line: ..." entries but carry no real surprise value.
1328/// The hand-crafted pack omitted them entirely; this filter makes the auto-
1329/// generated pack match that judgment.
1330fn is_noise_line(line: &str) -> bool {
1331    let trimmed = line.trim();
1332    if trimmed.is_empty() {
1333        return true;
1334    }
1335    // Length: lines shorter than 15 chars are too short to carry surprise
1336    // (closing braces, semicolons, blank-ish content).
1337    if trimmed.len() < 15 {
1338        return true;
1339    }
1340    // Pure structural closers
1341    if trimmed == "}" || trimmed == "});" || trimmed == "};" || trimmed == "];" || trimmed == ")" {
1342        return true;
1343    }
1344    // Imports / use statements (the skeleton normalizes these all to {w} anyway,
1345    // and the model's prior handles them).
1346    if trimmed.starts_with("use ") || trimmed.starts_with("import ") {
1347        return true;
1348    }
1349    // Comments — already excluded by tokenization, but double-check
1350    if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
1351        return true;
1352    }
1353    // Python docstrings (multi-line string literals used as documentation)
1354    if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
1355        return true;
1356    }
1357    // Python class-method internals — not surprising, just noise
1358    if trimmed.starts_with("self.") || trimmed.starts_with("cls.") || trimmed.starts_with("super()") {
1359        return true;
1360    }
1361    // Python imports — model's prior handles these
1362    if trimmed.starts_with("from ") && trimmed.contains(" import ") {
1363        return true;
1364    }
1365    // Python assertions — test helpers, not surprising
1366    if trimmed.starts_with("assert") {
1367        return true;
1368    }
1369    // Lines that are just a closing docstring/quote
1370    if trimmed == "\"\"\"" || trimmed == "'''" || trimmed == "\"\"\";" || trimmed == "'''\";" {
1371        return true;
1372    }
1373    // Simple variable bindings: `let x = y;` or `let mut x = ...;`
1374    // These are the most common "unique line" noise. Allow ones with parens
1375    // (`let x = foo(...)` may be a function call worth noting).
1376    if (trimmed.starts_with("let ") || trimmed.starts_with("let mut "))
1377        && !trimmed.contains("(")
1378        && !trimmed.contains("{")
1379        && trimmed.ends_with(";")
1380    {
1381        return true;
1382    }
1383    // Simple return statements with no value or a bare identifier
1384    // (`return;` or `return x;` where x is a single word — not surprising)
1385    // S1 fix: strip inline comments first so trailing // doesn't bypass filters
1386    let trimmed_for_return = if let Some(comment_start) = trimmed.find("//") {
1387        trimmed[..comment_start].trim()
1388    } else {
1389        trimmed
1390    };
1391    if let Some(rest) = trimmed_for_return.strip_prefix("return ") {
1392        // `return;` or `return Some(x);` or `return None;` IS interesting if it's
1393        // a sentinel. Only filter the trivial `return <single_word>;` case.
1394        if let Some(inner) = rest.strip_suffix(";") {
1395            let inner = inner.trim();
1396            if !inner.contains("(") && !inner.contains(".") && !inner.contains("[") {
1397                // Single bare identifier return — likely noise
1398                return true;
1399            }
1400        }
1401        // Python returns: `return X.lower() in Y.lower()` — too common to be surprising
1402        if rest.contains(".lower()") || rest.contains(".upper()") {
1403            return true;
1404        }
1405    }
1406    // Python `if isinstance(obj, X):` — too common to be surprising
1407    if trimmed.starts_with("if isinstance(") {
1408        return true;
1409    }
1410    // Python `for X in Y:` — basic iteration, not surprising
1411    if trimmed.starts_with("for ") && trimmed.ends_with(":") && !trimmed.contains("enumerate") {
1412        return true;
1413    }
1414    // Python `self.X = Y` — attribute assignment, not surprising
1415    if trimmed.starts_with("self.") && trimmed.contains(" = ") && !trimmed.contains("(") {
1416        return true;
1417    }
1418    // Simple assertions / panics with no context
1419    if trimmed.starts_with("assert!") || trimmed.starts_with("panic!") || trimmed.starts_with("todo!") {
1420        return true;
1421    }
1422    // Attribute lines
1423    if trimmed.starts_with("#[") || trimmed.starts_with("#![") {
1424        return true;
1425    }
1426    // Mod / pub mod declarations
1427    if trimmed.starts_with("mod ") || trimmed.starts_with("pub mod ") {
1428        return true;
1429    }
1430    // S1 fix: derive attributes already filtered above with #include, but
1431    // additional patterns below were still producing "unique line:" noise
1432    if trimmed.starts_with("derive(") || trimmed.starts_with("cfg_attr(") || trimmed.starts_with("allow(") {
1433        return true;
1434    }
1435    // S1 fix: Python test methods (self.assertX, self.setUp, self.X.method)
1436    if let Some(rest) = trimmed.strip_prefix("self.") {
1437        if rest.starts_with("assert") || rest.contains(".assert") || rest.starts_with("setUp")
1438            || rest.starts_with("tearDown") || rest.starts_with("set_up")
1439            || rest.starts_with("tear_down") || rest.starts_with("skip(")
1440            || rest.starts_with("fixture") || rest.starts_with("patch(")
1441        {
1442            return true;
1443        }
1444    }
1445    // S1 fix: Rust method calls ending in ; — operations, not surprising
1446    //   e.g., `block_phrases.append(line);`
1447    if trimmed.contains(".append(") || trimmed.contains(".push(")
1448        || trimmed.contains(".insert(") || trimmed.contains(".remove(")
1449        || trimmed.contains(".clear(") || trimmed.contains(".drop(")
1450        || trimmed.contains(".len()") || trimmed.contains(".is_empty()")
1451        || trimmed.contains(".expect(") || trimmed.contains(".collect()")
1452        || trimmed.contains(".clone(") || trimmed.contains(".iter()")
1453    {
1454        return true;
1455    }
1456    // S1 fix: Rust single-token statements
1457    if trimmed == "break;" || trimmed == "continue;"
1458        || trimmed == "break" || trimmed == "continue" {
1459        return true;
1460    }
1461    // S1 fix: Python from X import statements (already covered by use)
1462    if trimmed.starts_with("from ") {
1463        return true;
1464    }
1465    // S1 fix: trivial returns (`return None;` `return true;` `return false;`)
1466    if let Some(stripped_return) = trimmed.strip_prefix("return ") {
1467        let rest = stripped_return.trim().trim_end_matches(';').trim();
1468        if matches!(rest, "true" | "false" | "None" | "0" | "1" | "Self" | "-1") {
1469            return true;
1470        }
1471    }
1472    // S1 fix: log macros and trivial logging
1473    if trimmed.starts_with("log::") || trimmed.starts_with("tracing::")
1474        || trimmed.starts_with("debug!") || trimmed.starts_with("info!")
1475        || trimmed.starts_with("warn!") || trimmed.starts_with("error!")
1476        || trimmed.starts_with("print!") || trimmed.starts_with("eprint!")
1477        || trimmed.starts_with("println!") || trimmed.starts_with("eprintln!")
1478        || trimmed.starts_with("dbg!") || trimmed.starts_with("todo!")
1479    {
1480        return true;
1481    }
1482    // S1 fix: ignore/discard assignments like `let _ = expr;`
1483    if trimmed.starts_with("let _ =") || trimmed.starts_with("let _mut _ =")
1484        || trimmed.starts_with("let (_, _) =") || trimmed.starts_with("let (_, _, _) =")
1485    {
1486        return true;
1487    }
1488    // S1 fix: option-result idioms (.ok() / .ok_or() patterns)
1489    // Strip trailing inline comment first (// GUARDED: ...)
1490    let code_only = if let Some(comment_start) = trimmed.find("//") {
1491        trimmed[..comment_start].trim()
1492    } else {
1493        trimmed
1494    };
1495    if code_only.ends_with(".ok()?") || code_only.ends_with(".ok();")
1496        || code_only.contains(".expect(\"") || code_only.contains(".unwrap_or(")
1497        || code_only.contains(".unwrap_or_default(") || code_only.contains(".unwrap_or_else(")
1498        || (code_only.starts_with("return ") && code_only.contains(".ok()"))
1499        || (code_only.starts_with("return ") && code_only.contains(".err()"))
1500        || (code_only.starts_with("return ") && code_only.contains(".unwrap()"))
1501    {
1502        return true;
1503    }
1504    if let Some(eq_pos) = trimmed.find('=') {
1505        if eq_pos > 0 && !trimmed[eq_pos+1..].trim().starts_with("\"")
1506            && trimmed.contains(".to_string(")
1507            && !trimmed.contains("unsafe ") && !trimmed.contains("transmute")
1508        {
1509            return true;
1510        }
1511    }
1512    false
1513}
1514
1515
1516/// Extract surprise facts from a symbol's body using skeleton-frequency.
1517///
1518/// A line is "surprising" if its aggressive_skeleton appears < RARE_THRESHOLD
1519/// times across the codebase. We also extract magic numbers, compound
1520/// conditions, and early returns from surprising lines.
1521fn extract_surprise_from_body(
1522    body: &str,
1523    skeleton_freq: &FxHashMap<String, usize>,
1524) -> Vec<String> {
1525    let mut surprises: Vec<String> = Vec::new();
1526    let mut seen: HashSet<String> = HashSet::new();
1527
1528    let lines: Vec<&str> = body.lines().collect();
1529
1530    // === Pass 1: Pattern-based detection ===
1531    // Recognize common "surprising code patterns" and generate labeled descriptions.
1532
1533    // Pattern: `<N>` auto-pass guard at the top of a function
1534    //   e.g., `if text.len() < 50 { return Some((1.0, text.len())); }`
1535    //   → "auto-pass for input below N chars (no gates applied)"
1536    if let Some(m) = regex_capture(r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{", body) {
1537        add_surprise(&mut surprises, &mut seen,
1538            format!("auto-pass for input below {} chars (no gates applied)", m));
1539    }
1540
1541    // Pattern: UUID-like position checks (8, 13, 18, 23)
1542    //   e.g., `let expect_dash = j == 8 || j == 13 || j == 18 || j == 23;`
1543    if body.contains("expect_dash") || body.contains("j == 8") && body.contains("j == 13") && body.contains("j == 23") {
1544        add_surprise(&mut surprises, &mut seen,
1545            "UUID dash positions: 8, 13, 18, 23 (standard 8-4-4-4-12)".to_string());
1546    }
1547
1548    // Pattern: hex-hash length range
1549    //   e.g., `he - i >= 7 && he - i <= 40`
1550    if body.contains("he - i") || body.contains("hex_len") {
1551        if let Some(m) = regex_capture(r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)", body) {
1552            add_surprise(&mut surprises, &mut seen,
1553                format!("hex-hash length range: {} to {}", m.split(',').next().unwrap_or(""), m.split(',').nth(1).unwrap_or("")));
1554        }
1555    }
1556
1557    // Pattern: djb3-33 hash constants
1558    //   e.g., `let mut h: u64 = 5381; for b ... { h = h.wrapping_mul(33) }`
1559    if body.contains("5381") && body.contains("wrapping_mul") && body.contains("33") {
1560        add_surprise(&mut surprises, &mut seen,
1561            "djb3-33 hash: starts at 5381, multiplies by 33 (Bernstein variant)".to_string());
1562    }
1563
1564    // Pattern: byte-DFA processing
1565    //   e.g., iterating `bytes[i]` and `bytes[he]` directly
1566    if body.contains("bytes[") && body.contains("while") && !body.contains("chars()") {
1567        add_surprise(&mut surprises, &mut seen,
1568            "byte-indexed processing (not char-indexed)".to_string());
1569    }
1570
1571    // Pattern: sentinel return value
1572    //   e.g., `if s.is_empty() { return 0; }` or `return String::new()`
1573    if let Some(m) = regex_capture(r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)", body) {
1574        add_surprise(&mut surprises, &mut seen,
1575            format!("sentinel return: {} for empty input", m));
1576    }
1577
1578// Pattern: import collapse label
1579    //   e.g., `format!("[{} imports]", import_count)`
1580    if body.contains("import") && body.contains("count") && body.contains('[') {
1581        add_surprise(&mut surprises, &mut seen,
1582            "imports collapsed into a single label '[N imports]'".to_string());
1583    }
1584
1585    // Pattern: lazy/deferred flush
1586    //   e.g., only writing to result on the next non-matching line
1587    if body.contains("flush") || (body.contains("if ") && body.contains("emit") && body.contains("reset")) {
1588        add_surprise(&mut surprises, &mut seen,
1589            "lazy flush: label emitted on next non-matching line, not immediately".to_string());
1590    }
1591
1592    // Pattern: priority ordering
1593    //   e.g., Error check before Comment check
1594    if body.contains("Error") && body.contains("Comment") {
1595        let error_pos = body.find("Error").unwrap_or(0);
1596        let comment_pos = body.find("Comment").unwrap_or(0);
1597        if error_pos < comment_pos && error_pos > 0 && comment_pos > 0 {
1598            add_surprise(&mut surprises, &mut seen,
1599                "Error check runs BEFORE Comment check (priority order)".to_string());
1600        } else if comment_pos < error_pos && comment_pos > 0 && error_pos > 0 {
1601            add_surprise(&mut surprises, &mut seen,
1602                "Comment check runs BEFORE Error check (potential bug: errors in comments are missed)".to_string());
1603        }
1604    }
1605
1606    // Pattern: gate combination (AND vs OR)
1607    //   e.g., `if ent < threshold && ratio > max { return None; }`
1608    if regex_capture(r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+", body).is_some() {
1609        if body.contains("&&") {
1610            add_surprise(&mut surprises, &mut seen,
1611                "gates are AND-combined: ALL must fail for None return".to_string());
1612        } else if body.contains("||") {
1613            add_surprise(&mut surprises, &mut seen,
1614                "gates are OR-combined: ANY failing gate returns None".to_string());
1615        }
1616    }
1617
1618    // Pattern: minimum word length for aggressive collapsing
1619    //   e.g., `we > i + 1` (length > 1 keeps single letters verbatim)
1620    if body.contains("aggressive") || body.contains("collapse") {
1621        if let Some(m) = regex_capture(r"\w+\s*>\s*\w+\s*\+\s*(\d+)", body) {
1622            add_surprise(&mut surprises, &mut seen,
1623                format!("minimum word length threshold: {} (below this, words kept verbatim)", m));
1624        }
1625    }
1626
1627    // Pattern: function called on the SKELETON, not the original
1628    //   e.g., `let s = skeleton(text); ... hash(s)`
1629    if let Some(m) = regex_capture(r"let\s+(\w+)\s*=\s*skeleton\(\w+\)", body) {
1630        add_surprise(&mut surprises, &mut seen,
1631            format!("operates on the SKELETON of input, not the original text (via {})", m));
1632    }
1633
1634    // Pattern: regex-based normalization (not byte-DFA)
1635    //   e.g., `PATTERNS.uuid.replace_all(&cleaned, "{uuid}")`
1636    if body.contains("replace_all") && body.contains("&") {
1637        let placeholders: Vec<&str> = body
1638            .split(".replace_all")
1639            .skip(1)
1640            .filter_map(|s| {
1641                if let Some(idx) = s.find('"') {
1642                    let after = &s[idx..];
1643                    if let Some(end) = after[1..].find('"') {
1644                        return Some(&after[1..=end]);
1645                    }
1646                }
1647                None
1648            })
1649            .collect();
1650        if !placeholders.is_empty() {
1651            add_surprise(&mut surprises, &mut seen,
1652                format!("regex-based replacement: {}", placeholders.iter().take(3).cloned().collect::<Vec<_>>().join(", ")));
1653        }
1654    }
1655
1656// Pattern: three-comma version detection
1657    //   e.g., `X.Y.Z` with `num_end` calls
1658    if body.contains("num_end") && body.contains('.') {
1659        add_surprise(&mut surprises, &mut seen,
1660            "version detection: requires exactly three dot-separated numeric components (X.Y.Z)".to_string());
1661    }
1662
1663    // === Pass 2: Line-level signal extraction ===
1664    let mut seen_nums: HashSet<String> = HashSet::new();
1665    let mut seen_conds: HashSet<String> = HashSet::new();
1666
1667    for (line_idx, line) in lines.iter().enumerate() {
1668        let trimmed = line.trim();
1669        if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#")
1670            || trimmed.starts_with("///") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
1671            continue;
1672        }
1673        let code = trimmed;
1674
1675        // Magic numbers in comparisons
1676        for num in find_magic_numbers(code) {
1677            let key = format!("{}_{}", num, line_idx);
1678            if !seen_nums.contains(&key) {
1679                seen_nums.insert(key);
1680                let labeled = label_number(&num, code);
1681                if !labeled.starts_with("constant: 0") && !labeled.starts_with("constant: 1") {
1682                    add_surprise(&mut surprises, &mut seen, labeled);
1683                }
1684            }
1685        }
1686
1687        // Compound conditions (not already captured by patterns)
1688        if code.contains("if ") && (code.contains("&&") || code.contains("||")) {
1689            if let Some(cond) = extract_condition(code) {
1690                let cond_key: String = cond.chars().take(40).collect();
1691                if cond.len() > 10 && cond.len() < 200 && !seen_conds.contains(&cond_key) {
1692                    seen_conds.insert(cond_key);
1693                    add_surprise(&mut surprises, &mut seen,
1694                        format!("compound condition: {}", cond));
1695                }
1696            }
1697        }
1698
1699        // Early returns
1700        if line_idx < 15 && code.contains("if ") && code.contains("return") {
1701            if let Some(ret) = extract_return_value(code) {
1702                if !ret.is_empty() && ret != "true" && ret != "false" {
1703                    add_surprise(&mut surprises, &mut seen,
1704                        format!("early return → {}", ret));
1705                }
1706            }
1707        }
1708
1709        // Deliberate patterns
1710        if code.contains(".wrapping_") {
1711            let ops: Vec<&str> = code
1712                .split(".wrapping_")
1713                .skip(1)
1714                .filter_map(|s| s.split('(').next())
1715                .collect();
1716            if !ops.is_empty() {
1717                add_surprise(&mut surprises, &mut seen,
1718                    format!("wrapping: {}", ops.join(", ")));
1719            }
1720        }
1721        if code.contains("unwrap_or(") {
1722            if let Some(def) = extract_unwrap_default(code) {
1723                if !def.is_empty() {
1724                    add_surprise(&mut surprises, &mut seen,
1725                        format!("fallback: unwrap_or({})", def));
1726                }
1727            }
1728        }
1729
1730        // Capacity hints
1731        if code.contains("with_capacity(") {
1732            if let Some(n) = extract_capacity(code) {
1733                add_surprise(&mut surprises, &mut seen,
1734                    format!("capacity: {}", n));
1735            }
1736        }
1737
1738        // Comment signals
1739        if let Some(comment_start) = code.find("//") {
1740            let comment = code[comment_start + 2..].trim().to_ascii_lowercase();
1741            let comment_text = code[comment_start + 2..].trim();
1742            for sig in ["key", "difference", "note", "important", "surprise",
1743                        "critical", "deliberate", "only fires", "not regex", "must"]
1744            {
1745                if comment.contains(sig) {
1746                    add_surprise(&mut surprises, &mut seen,
1747                        format!("source note: {}", comment_text));
1748                    break;
1749                }
1750            }
1751        }
1752    }
1753
1754    // === Pass 3: Skeleton-frequency rare patterns (tertiary) ===
1755    for (line_idx, line) in lines.iter().enumerate() {
1756        if line_idx == 0 { continue; } // skip signature line
1757        let trimmed = line.trim();
1758        if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
1759            continue;
1760        }
1761        if is_noise_line(trimmed) {
1762            continue;
1763        }
1764        let skel = aggressive_skeleton(trimmed);
1765        if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 10 {
1766            continue;
1767        }
1768        let freq = skeleton_freq.get(&skel).copied().unwrap_or(0);
1769        if freq == 1 {
1770            // S1 fix: only emit unique-line for lines with structural signal.
1771            // A truly unique line with no parens / braces / comparisons /
1772            // assignments is just noise (any random `let x = other_line;`
1773            // can be unique because of variable name).
1774            let has_signal = trimmed.contains('(')
1775                || trimmed.contains("==") || trimmed.contains("!=")
1776                || trimmed.contains(" < ") || trimmed.contains(" > ")
1777                || trimmed.contains(">=") || trimmed.contains("<=")
1778                || trimmed.contains("&&") || trimmed.contains("||")
1779                || trimmed.contains("wrapping_") || trimmed.contains("unwrap_or")
1780                || trimmed.contains("match ") || trimmed.contains("=>");
1781            if has_signal {
1782                let snippet: String = trimmed.chars().take(80).collect();
1783                add_surprise(&mut surprises, &mut seen,
1784                    format!("unique line: {}", snippet));
1785            }
1786        }
1787    }
1788
1789    // S1 fix: dedupe across passes (early return + unique line often duplicate)
1790    // After both passes are done, dedupe entries that share key information.
1791    let pre_dedup_len = surprises.len();
1792    let mut final_surprises: Vec<String> = Vec::new();
1793    let mut seen_fragments: HashSet<String> = HashSet::new();
1794    for s in surprises {
1795        // Extract a canonical "fragment" — same early return + same unique line → same key
1796        let fragment: String = s
1797            .chars()
1798            .filter(|c| !c.is_whitespace())
1799            .take(40)
1800            .collect();
1801        if seen_fragments.contains(&fragment) {
1802            continue;
1803        }
1804        seen_fragments.insert(fragment);
1805        final_surprises.push(s);
1806    }
1807    let surprises = final_surprises;
1808    let post_dedup_len = surprises.len();
1809    if pre_dedup_len > post_dedup_len + 1 {
1810        // dedupe was meaningful; that's good
1811    }
1812    // Count pattern-based (non "rare pattern" / "unique line") vs. frequency-based
1813    let pattern_count = surprises.iter()
1814        .filter(|s| !s.starts_with("rare pattern") && !s.starts_with("unique line"))
1815        .count();
1816    let freq_count = surprises.len() - pattern_count;
1817
1818    // S1 fix: only include freq-based if we have 3+ pattern matches.
1819    // Without this guard, functions with zero patterns (just let-binding lines)
1820    // get 5 useless "unique line:" entries.
1821    let limit = if pattern_count >= 3 {
1822        pattern_count.min(8) // only pattern-based
1823    } else if pattern_count >= 1 {
1824        (pattern_count + freq_count.min(2)).min(8)
1825    } else {
1826        // No patterns matched — skip freq-based entirely (would be all noise)
1827        0
1828    };
1829
1830    surprises.into_iter().take(limit).collect()
1831}
1832
1833fn add_surprise(surprises: &mut Vec<String>, seen: &mut HashSet<String>, s: String) {
1834    let key: String = s.chars().take(60).collect();
1835    if !seen.contains(&key) {
1836        seen.insert(key);
1837        surprises.push(s);
1838    }
1839}
1840
1841/// Minimal regex capture helper — finds the first match of `pattern` in `text`
1842/// and returns the first capture group as a String.
1843fn regex_capture(pattern: &str, text: &str) -> Option<String> {
1844    // Avoid adding a regex dependency — simple manual patterns
1845    if pattern == r"if\s+\w+\.len\(\)\s*<\s*(\d+)\s*\{" {
1846        return manual_capture_len_guard(text);
1847    }
1848    if pattern == r">=\s*(\d+)\s*&&\s*\w+\s*-\s*\w+\s*<=\s*(\d+)" {
1849        return manual_capture_range(text);
1850    }
1851    if pattern == r"if\s+\w+\.is_empty\(\)\s*\{\s*return\s+([^;]+)" {
1852        return manual_capture_sentinel(text);
1853    }
1854    if pattern == r"\w+\s*>\s*\w+\s*\+\s*(\d+)" {
1855        return manual_capture_gt_plus(text);
1856    }
1857    if pattern == r"if\s+\w+\s*[<>]=\s*\w+_threshold\s*[&|][&|]\s*\w+\s*[<>]=\s*\w+" {
1858        return if text.contains("_threshold") { Some("found".to_string()) } else { None };
1859    }
1860    if pattern == r"let\s+(\w+)\s*=\s*skeleton\(\w+\)" {
1861        return manual_capture_skeleton_let(text);
1862    }
1863    None
1864}
1865
1866fn manual_capture_len_guard(text: &str) -> Option<String> {
1867    let bytes = text.as_bytes();
1868    let mut i = 0;
1869    while i < bytes.len() {
1870        if i + 2 < bytes.len() && bytes[i] == b'i' && bytes[i+1] == b'f' {
1871            // skip "if"
1872            i += 2;
1873            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1874            // skip identifier
1875            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
1876            // skip .len()
1877            while i + 4 < bytes.len() && &bytes[i..i+5] == b".len(" { i += 5; while i < bytes.len() && bytes[i] != b')' { i += 1; } if i < bytes.len() { i += 1; } }
1878            // skip spaces and <
1879            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1880            if i < bytes.len() && bytes[i] == b'<' { i += 1; }
1881            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1882            // read number
1883            let start = i;
1884            while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1885            if i > start {
1886                return Some(String::from_utf8_lossy(&bytes[start..i]).to_string());
1887            }
1888        }
1889        i += 1;
1890    }
1891    None
1892}
1893
1894fn manual_capture_range(text: &str) -> Option<String> {
1895    let bytes = text.as_bytes();
1896    let mut i = 0;
1897    while i < bytes.len() {
1898        if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i+1] == b'=' {
1899            i += 2;
1900            while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1901            let start = i;
1902            while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1903            if i > start {
1904                let first = String::from_utf8_lossy(&bytes[start..i]).to_string();
1905                // find the second number after <=
1906                while i < bytes.len() && bytes[i] != b'<' { i += 1; }
1907                if i + 1 < bytes.len() && bytes[i] == b'<' && bytes[i+1] == b'=' { i += 2; }
1908                while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
1909                let start2 = i;
1910                while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
1911                if i > start2 {
1912                    let second = String::from_utf8_lossy(&bytes[start2..i]).to_string();
1913                    return Some(format!("{},{}", first, second));
1914                }
1915            }
1916        }
1917        i += 1;
1918    }
1919    None
1920}
1921
1922fn manual_capture_sentinel(text: &str) -> Option<String> {
1923    if let Some(idx) = text.find("is_empty") {
1924        // idx + 9 may exceed the string when "is_empty" is the trailing text;
1925        // a fixed byte offset then panicked the whole pack build.
1926        let after = text.get(idx + 9..)?;
1927        if let Some(ret_idx) = after.find("return") {
1928            let after_ret = &after[ret_idx + 6..];
1929            let end = after_ret
1930                .find([';', '}', '\n'])
1931                .unwrap_or(after_ret.len());
1932            return Some(after_ret[..end].trim().to_string());
1933        }
1934    }
1935    None
1936}
1937
1938fn manual_capture_gt_plus(text: &str) -> Option<String> {
1939    if let Some(idx) = text.find('>') {
1940        let after = &text[idx + 1..];
1941        // skip spaces
1942        let trimmed = after.trim_start();
1943        // skip identifier
1944        let after_ident = trimmed
1945            .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1946        let trimmed2 = after_ident.trim_start();
1947        if let Some(rest) = trimmed2.strip_prefix('+') {
1948            let trimmed3 = rest.trim_start();
1949            let end = trimmed3
1950                .find(|c: char| !c.is_ascii_digit())
1951                .unwrap_or(trimmed3.len());
1952            if end > 0 {
1953                return Some(trimmed3[..end].to_string());
1954            }
1955        }
1956    }
1957    None
1958}
1959
1960fn manual_capture_skeleton_let(text: &str) -> Option<String> {
1961    if let Some(idx) = text.find("let ") {
1962        let after = &text[idx + 4..];
1963        let after_ident = after
1964            .trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
1965        let trimmed = after_ident.trim_start();
1966        if let Some(after_eq) = trimmed.strip_prefix('=') {
1967            let after_eq = after_eq.trim_start();
1968            if let Some(sk_idx) = after_eq.find("skeleton(") {
1969                let before = &after_eq[..sk_idx].trim();
1970                if !before.is_empty() {
1971                    return Some(before.to_string());
1972                }
1973            }
1974        }
1975    }
1976    None
1977}
1978
1979fn find_magic_numbers(code: &str) -> Vec<String> {
1980    let mut nums = Vec::new();
1981    // Simple regex-free approach: find numbers after comparison operators
1982    let bytes = code.as_bytes();
1983    let mut i = 0;
1984    while i < bytes.len() {
1985        if (bytes[i] == b'>' || bytes[i] == b'<' || bytes[i] == b'=' || bytes[i] == b'!')
1986            && i + 1 < bytes.len()
1987        {
1988            // Skip the operator
1989            i += 1;
1990            if i < bytes.len() && (bytes[i] == b'=' || bytes[i] == b' ') {
1991                i += 1;
1992            }
1993            // Skip spaces
1994            while i < bytes.len() && bytes[i] == b' ' {
1995                i += 1;
1996            }
1997            // Read number
1998            let start = i;
1999            while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
2000                i += 1;
2001            }
2002            if i > start {
2003                let num = String::from_utf8_lossy(&bytes[start..i]).to_string();
2004                if num != "0" && num != "1" {
2005                    nums.push(num);
2006                }
2007            }
2008        } else {
2009            i += 1;
2010        }
2011    }
2012    nums
2013}
2014
2015fn label_number(num: &str, code: &str) -> String {
2016    let lower = code.to_ascii_lowercase();
2017    if lower.contains("uuid") || (num == "8" || num == "13" || num == "18" || num == "23") {
2018        format!("UUID position: {}", num)
2019    } else if lower.contains("hash") || lower.contains("hex") {
2020        format!("hex-hash bound: {}", num)
2021    } else if lower.contains("entropy") || lower.contains("threshold") {
2022        format!("threshold: {}", num)
2023    } else if lower.contains("len") || lower.contains("length") || lower.contains("count") {
2024        format!("length: {}", num)
2025    } else if lower.contains("capacity") {
2026        format!("capacity: {}", num)
2027    } else {
2028        format!("constant: {}", num)
2029    }
2030}
2031
2032fn extract_condition(code: &str) -> Option<String> {
2033    let if_pos = code.find("if ")?;
2034    let after = &code[if_pos + 3..];
2035    // Find the end of condition (opening brace or end of line)
2036    let end = after
2037        .find(['{', '\n'])
2038        .unwrap_or(after.len());
2039    Some(after[..end].trim().to_string())
2040}
2041
2042fn extract_return_value(code: &str) -> Option<String> {
2043    let ret_pos = code.find("return")?;
2044    let after = &code[ret_pos + 6..];
2045    let end = after.find([';', '}']).unwrap_or(after.len());
2046    Some(after[..end].trim().to_string())
2047}
2048
2049fn extract_unwrap_default(code: &str) -> Option<String> {
2050    let pos = code.find("unwrap_or(")?;
2051    let after = &code[pos + 10..];
2052    let end = after.find(')')?;
2053    Some(after[..end].trim().to_string())
2054}
2055
2056fn extract_capacity(code: &str) -> Option<String> {
2057    let pos = code.find("with_capacity(")?;
2058    let after = &code[pos + 14..];
2059    let end = after.find(')')?;
2060    Some(after[..end].trim().to_string())
2061}
2062
2063// ---------------------------------------------------------------------------
2064// Cross-references from the SQLite index
2065// ---------------------------------------------------------------------------
2066
2067/// Build cross-reference map: symbol_name → list of symbols that reference it.
2068///
2069/// Uses file-level co-occurrence: find files where the symbol appears as a
2070/// non-def occurrence, then list other symbols defined in those files.
2071/// This is broader than block-matching and catches most real references.
2072fn build_cross_refs_from_index(
2073    db: &Connection,
2074    symbols: &[Symbol],
2075) -> FxHashMap<String, Vec<String>> {
2076    // C3: Return empty map on error instead of panicking.
2077    build_cross_refs_inner(db, symbols).unwrap_or_default()
2078}
2079
2080fn build_cross_refs_inner(
2081    db: &Connection,
2082    symbols: &[Symbol],
2083) -> Result<FxHashMap<String, Vec<String>>, rusqlite::Error> {
2084    let def_names: HashSet<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2085    let mut cross_refs: FxHashMap<String, Vec<String>> = FxHashMap::default();
2086    let mut ref_set: FxHashMap<String, HashSet<String>> = FxHashMap::default();
2087
2088    // === Phase 1: Batch-load ALL (file_id, phrase) pairs in one query ===
2089    // This avoids the N+1 query pattern that made the previous version O(N²).
2090    // V54: cap rows to bound memory on large repos (full occurrence table
2091    // can be 500K+ rows; symbol-relevant rows are a small subset).
2092    let mut all_pairs_stmt = db.prepare(
2093        "SELECT o.file_id, p.phrase, o.is_def
2094         FROM occurrence o
2095         JOIN phrases p ON o.phrase_id = p.id
2096         WHERE length(p.phrase) BETWEEN 3 AND 30
2097         ORDER BY o.file_id
2098         LIMIT 200000",
2099    )?;
2100
2101    // For each file_id, the set of (phrase, is_def) pairs that appear in it
2102    let mut file_to_phrases: FxHashMap<i64, Vec<(String, bool)>> = FxHashMap::default();
2103    for row in all_pairs_stmt
2104        .query_map([], |row| {
2105            Ok((
2106                row.get::<_, i64>(0)?,
2107                row.get::<_, String>(1)?,
2108                row.get::<_, i64>(2)? != 0,
2109            ))
2110        })?
2111        .flatten()
2112    {
2113        let (file_id, phrase, is_def) = row;
2114        file_to_phrases
2115            .entry(file_id)
2116            .or_default()
2117            .push((phrase, is_def));
2118    }
2119
2120    // === Phase 2: For each symbol, find files where it's used (non-def) ===
2121    let mut phrase_to_non_def_files: FxHashMap<String, Vec<i64>> = FxHashMap::default();
2122    for (file_id, pairs) in &file_to_phrases {
2123        for (phrase, is_def) in pairs {
2124            if !*is_def {
2125                phrase_to_non_def_files
2126                    .entry(phrase.clone())
2127                    .or_default()
2128                    .push(*file_id);
2129            }
2130        }
2131    }
2132
2133    // Now for each symbol: look up files directly via inverted index
2134    for sym in symbols {
2135        let mut refs: HashSet<String> = HashSet::new();
2136
2137        let files_using_sym = match phrase_to_non_def_files.get(&sym.name) {
2138            Some(f) => f,
2139            None => continue,
2140        };
2141
2142        for &file_id in files_using_sym {
2143            if let Some(pairs) = file_to_phrases.get(&file_id) {
2144                for (phrase, is_def) in pairs {
2145                    if *is_def
2146                        && phrase != &sym.name
2147                        && def_names.contains(phrase.as_str())
2148                        && !is_common_word(phrase)
2149                        && phrase.len() >= 3
2150                        && phrase.len() < 30
2151                        && !phrase.starts_with('_')
2152                        && !phrase.ends_with("Error")
2153                        && !phrase.ends_with("Result")
2154                        && !phrase.ends_with("Option")
2155                        && phrase != "Self"
2156                    {
2157                        refs.insert(phrase.clone());
2158                    }
2159                }
2160            }
2161
2162            // Don't bother scanning more files once we have enough refs
2163            if refs.len() >= 5 {
2164                break;
2165            }
2166        }
2167
2168        // Take top 3 refs, sorted
2169        if !refs.is_empty() {
2170            let mut sorted: Vec<String> = refs.into_iter().collect();
2171            sorted.sort();
2172            sorted.truncate(3);
2173            ref_set.insert(sym.name.clone(), sorted.into_iter().collect());
2174        }
2175    }
2176
2177    // Convert sets to sorted vectors, capped at 3 to keep pack compact
2178    for (name, refs) in ref_set {
2179        let mut sorted: Vec<String> = refs.into_iter().collect();
2180        sorted.sort();
2181        sorted.truncate(3);
2182        cross_refs.insert(name, sorted);
2183    }
2184
2185    Ok(cross_refs)
2186}
2187
2188// ---------------------------------------------------------------------------
2189// Source reading
2190// ---------------------------------------------------------------------------
2191
2192/// Read sources for a SUBSET of symbols (the selected hotspots) + build
2193/// skeleton frequency from just those symbols' bodies. This is much faster
2194/// than reading all files — only ~50 file reads instead of ~700.
2195fn read_symbols_for_subset(
2196    selected: &[Symbol],
2197    _all_symbols: &[Symbol],
2198) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2199    let mut sources: FxHashMap<String, String> = FxHashMap::default();
2200    let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2201
2202    // S2: use the shared file_meta cache instead of a per-call file_cache.
2203    // file_meta::get returns Arc<FileMeta> with lines pre-split and cached
2204    // across the MCP lifetime. Eliminates redundant disk reads.
2205    for sym in selected {
2206        let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2207            meta.lines.to_vec()
2208        } else {
2209            // Fallback: direct disk read for files not yet cached.
2210            std::fs::read_to_string(&sym.file)
2211                .map(|c| c.lines().map(String::from).collect())
2212                .unwrap_or_default()
2213        };
2214
2215        let start = (sym.line as usize).min(lines.len().saturating_sub(1));
2216        let end = find_body_end(lines.as_slice(), start, sym.indent).min(lines.len());
2217        if start < end {
2218            // Iterate the slice directly — no join→re-split
2219            for line in &lines[start..end] {
2220                let trimmed = line.trim();
2221                if trimmed.is_empty() {
2222                    continue;
2223                }
2224                let skel = aggressive_skeleton(trimmed);
2225                if !skel.is_empty() {
2226                    *freq.entry(skel).or_insert(0) += 1;
2227                }
2228            }
2229            sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2230        }
2231    }
2232
2233    (sources, freq)
2234}
2235
2236/// Read all symbol sources and build skeleton frequency in a single pass.
2237/// Avoids re-reading the same file multiple times across symbols.
2238fn read_symbols_and_frequency(
2239    symbols: &[Symbol],
2240) -> (FxHashMap<String, String>, FxHashMap<String, usize>) {
2241    let mut sources: FxHashMap<String, String> = FxHashMap::default();
2242    let mut freq: FxHashMap<String, usize> = FxHashMap::default();
2243
2244    // S2: shared file_meta cache replaces per-call file_cache.
2245
2246    for sym in symbols {
2247        let lines: Vec<String> = if let Some(meta) = reliary_search::file_meta::get(&sym.file) {
2248            meta.lines.to_vec()
2249        } else {
2250            std::fs::read_to_string(&sym.file)
2251                .map(|c| c.lines().map(String::from).collect())
2252                .unwrap_or_default()
2253        };
2254
2255        // Find the actual body extent. The block table's end_line is often wrong
2256        // (it undercounts for large functions), so we use brace-matching to find
2257        // the true end. We take the max of brace-matched end and end_line so
2258        // we never lose body content when the index is wrong.
2259        let start = (sym.line.max(0) as usize).min(lines.len().saturating_sub(1));
2260        let brace_end = find_body_end(lines.as_slice(), start, sym.indent);
2261        let index_end = (sym.end_line as usize).max(start + 1).min(lines.len());
2262        let end = brace_end.max(index_end).min(lines.len());
2263
2264        if start < end {
2265            // Build skeleton frequency from body lines — iterate slice directly, no join→re-split
2266            for line in &lines[start..end] {
2267                let trimmed = line.trim();
2268                if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
2269                    continue;
2270                }
2271                let skel = aggressive_skeleton(trimmed);
2272                if skel.is_empty() || skel == "{w}" || skel == "}" || skel.len() < 5 {
2273                    continue;
2274                }
2275                *freq.entry(skel).or_insert(0) += 1;
2276            }
2277
2278            sources.insert(sym.name.clone(), lines[start..end].join("\n"));
2279        }
2280    }
2281
2282    (sources, freq)
2283}
2284
2285/// Find the end of a function body by brace-matching from the start line.
2286/// Returns the line index AFTER the closing brace.
2287/// If brace-matching fails (e.g., Python uses indentation), falls back to
2288/// looking for the next def/class line at the same indent.
2289fn find_body_end(lines: &[String], start: usize, _base_indent: i32) -> usize {
2290    if start >= lines.len() {
2291        return start;
2292    }
2293    let first_line = &lines[start];
2294
2295    // Python-style: find the next def/class at the same or lower indent
2296    let stripped = first_line.trim();
2297    if stripped.starts_with("def ")
2298        || stripped.starts_with("class ")
2299        || stripped.starts_with("async def ")
2300    {
2301        let first_indent = first_line.len() - first_line.trim_start().len();
2302        for (i, line) in lines.iter().enumerate().skip(start + 1) {
2303            let trimmed = line.trim();
2304            if trimmed.is_empty() || trimmed.starts_with("#") {
2305                continue;
2306            }
2307            let indent = line.len() - line.trim_start().len();
2308            // A new top-level def/class at the same indent marks the end
2309            if indent <= first_indent
2310                && (trimmed.starts_with("def ")
2311                    || trimmed.starts_with("class ")
2312                    || trimmed.starts_with("async def "))
2313            {
2314                return i;
2315            }
2316        }
2317        return lines.len();
2318    }
2319
2320    // C-style: brace-matching
2321    let mut depth: i32 = 0;
2322    let mut seen_open = false;
2323    for (i, line) in lines.iter().enumerate().skip(start) {
2324        let trimmed = line.trim();
2325
2326        // Skip comments and strings (crude — doesn't handle multi-line strings)
2327        if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
2328            continue;
2329        }
2330
2331        for ch in line.chars() {
2332            match ch {
2333                '{' => { depth += 1; seen_open = true; }
2334                '}' => { depth -= 1; }
2335                _ => {}
2336            }
2337        }
2338
2339        if seen_open && depth == 0 {
2340            return i + 1;
2341        }
2342    }
2343    lines.len()
2344}
2345
2346// ---------------------------------------------------------------------------
2347// Module detection
2348// ---------------------------------------------------------------------------
2349
2350/// Extract the crate name from a file path.
2351/// Looks for `crates/<name>/` or `packages/<name>/` patterns.
2352fn derive_crate_name(file_path: &str) -> String {
2353    let parts: Vec<&str> = file_path.split('/').collect();
2354    for (i, part) in parts.iter().enumerate() {
2355        if (*part == "crates" || *part == "packages") && i + 1 < parts.len() {
2356            return parts[i + 1].to_string();
2357        }
2358    }
2359    // Fallback: the parent directory name, but only when it is a *meaningful*
2360    // disambiguator. A generic source-root directory (src, lib, app, source,
2361    // tests, …) is not — using it produced headers like `Foo/src`, which reads
2362    // as part of the symbol name and is pure noise on flat layouts.
2363    if parts.len() >= 2 {
2364        let parent = parts[parts.len() - 2];
2365        const GENERIC: &[&str] = &[
2366            "src", "lib", "app", "apps", "source", "sources", "test", "tests",
2367            "spec", "specs", "bin", "cmd", "main", "code", "internal", "pkg",
2368        ];
2369        if !GENERIC.contains(&parent) {
2370            return parent.to_string();
2371        }
2372    }
2373    String::new()
2374}
2375
2376fn detect_modules(symbols: &[Symbol]) -> Vec<(String, Vec<Symbol>)> {
2377    let mut module_map: FxHashMap<String, Vec<Symbol>> = FxHashMap::default();
2378
2379    for sym in symbols {
2380        let module = derive_module_name(&sym.file);
2381        module_map.entry(module).or_default().push(sym.clone());
2382    }
2383
2384    let mut modules: Vec<(String, Vec<Symbol>)> = module_map.into_iter().collect();
2385    modules.sort_by(|a, b| a.0.cmp(&b.0));
2386    modules
2387}
2388
2389fn derive_module_name(file_path: &str) -> String {
2390    let path = Path::new(file_path);
2391    let components: Vec<_> = path.components().collect();
2392
2393    // packages/<name>/... → packages/<name>
2394    for (i, comp) in components.iter().enumerate() {
2395        let part = comp.as_os_str().to_string_lossy().to_string();
2396        if part == "packages" && i + 1 < components.len() {
2397            return format!("packages/{}", components[i + 1].as_os_str().to_string_lossy());
2398        }
2399        if part == "src" && i + 1 < components.len() {
2400            return format!("src/{}", components[i + 1].as_os_str().to_string_lossy());
2401        }
2402        if part == "crates" && i + 1 < components.len() {
2403            return format!("crates/{}", components[i + 1].as_os_str().to_string_lossy());
2404        }
2405    }
2406
2407    // Fallback: first component
2408    components
2409        .first()
2410        .map(|c| c.as_os_str().to_string_lossy().to_string())
2411        .unwrap_or_else(|| "root".to_string())
2412}
2413
2414// ---------------------------------------------------------------------------
2415// Entry rendering
2416// ---------------------------------------------------------------------------
2417
2418/// Build an L4 co-change line for editing guidance.
2419fn build_cochange_line(_name: &str, signature: &str, _body: &str, cross_refs: &[String]) -> String {
2420    let lower = signature.to_ascii_lowercase();
2421    let mut parts: Vec<String> = Vec::new();
2422    let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2423    let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2424
2425    if is_enum {
2426        if !cross_refs.is_empty() {
2427            let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2428            parts.push(format!("Add variant → update match sites: {}", sites.join(", ")));
2429        }
2430    } else if is_struct {
2431        if !cross_refs.is_empty() {
2432            let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2433            parts.push(format!("Add field → update constructors: {}", sites.join(", ")));
2434        }
2435    } else {
2436        if !cross_refs.is_empty() {
2437            let sites: Vec<&str> = cross_refs.iter().take(6).map(|s| s.as_str()).collect();
2438            parts.push(format!("Rename → update {} caller(s): {}", cross_refs.len(), sites.join(", ")));
2439        }
2440    }
2441    parts.join("; ")
2442}
2443
2444fn expand_type_signature(signature: &str, body: &str, def_line: i32) -> String {
2445    let sig_trimmed = signature.trim();
2446    let lower = sig_trimmed.to_ascii_lowercase();
2447    let is_enum = lower.contains(" enum ") || lower.starts_with("enum ") || lower.starts_with("pub enum ");
2448    let is_struct = lower.contains(" struct ") || lower.starts_with("struct ") || lower.starts_with("pub struct ");
2449    if !is_enum && !is_struct {
2450        return signature.to_string();
2451    }
2452    let mut names: Vec<String> = Vec::new();
2453    for (j, line) in body.lines().enumerate().skip(1) {
2454        let trimmed = line.trim();
2455        if trimmed == "}" || trimmed.is_empty() { continue; }
2456        // Skip comments/attributes — a doc comment (`/// Tag: 0=...`) contains
2457        // a colon but is not a field declaration.
2458        if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with('*') {
2459            continue;
2460        }
2461        // body line j is 0-indexed within the body, whose first line is the
2462        // (0-indexed) definition line — so the 1-indexed source line is
2463        // def_line + 1 + j.
2464        let abs_line = def_line + 1 + j as i32;
2465        if is_enum {
2466            let name = trimmed.split([',', '{', '(', ':']).next().unwrap_or("").trim().trim_end_matches(',');
2467            if !name.is_empty() && name.len() > 1 && name.chars().next().is_some_and(|c| c.is_uppercase() || c == '_') {
2468                names.push(format!("{} (line {})", name, abs_line));
2469            }
2470        } else if is_struct {
2471            if let Some(colon_pos) = trimmed.find(':') {
2472                let name = trimmed[..colon_pos].trim().trim_start_matches("pub ").trim();
2473                let ftype = trimmed[colon_pos + 1..]
2474                    .trim()
2475                    .trim_end_matches(',')
2476                    .trim()
2477                    .to_string();
2478                if !name.is_empty() && name.len() > 1 {
2479                    // Include the type and line so describe/pack answers
2480                    // field-typed and field-located questions in one call.
2481                    if ftype.is_empty() || ftype == "{" {
2482                        names.push(format!("{} (line {})", name, abs_line));
2483                    } else {
2484                        let ft = ftype.split('{').next().unwrap_or(&ftype).trim();
2485                        names.push(format!("{}: {} (line {})", name, ft, abs_line));
2486                    }
2487                }
2488            }
2489        }
2490    }
2491    if names.is_empty() { return signature.to_string(); }
2492    let kind = if is_enum { "variants" } else { "fields" };
2493    format!("{} // {}({})", sig_trimmed, kind, names.join(", "))
2494}
2495
2496fn render_entry(
2497    sym: &Symbol,
2498    body: &str,
2499    cross_refs: &[String],
2500    skeleton_freq: &FxHashMap<String, usize>,
2501    format: PackFormat,
2502) -> String {
2503    // Include crate/module name to disambiguate same-name symbols across crates
2504    let crate_name = derive_crate_name(&sym.file);
2505    let header = if crate_name.is_empty() {
2506        format!("## {}", sym.name)
2507    } else {
2508        format!("## {}/{}", sym.name, crate_name)
2509    };
2510    let mut parts = vec![header];
2511
2512    // L0: one-line purpose (only in Full format, if doc comment exists)
2513    if matches!(format, PackFormat::Full) && !sym.doc_comment.is_empty() {
2514        let l0 = sym
2515            .doc_comment
2516            .split('.')
2517            .next()
2518            .unwrap_or(&sym.doc_comment)
2519            .trim();
2520        let l0 = if l0.chars().count() > 120 {
2521            // Truncate at char boundary, not byte boundary
2522            l0.chars().take(120).collect::<String>()
2523        } else {
2524            l0.to_string()
2525        };
2526        parts.push(format!("L0: {}", l0));
2527    }
2528
2529    // L2: always present — expand enum/struct signatures to include variants/fields
2530    let l2_line = expand_type_signature(&sym.signature, body, sym.line);
2531    // sym.line is 0-indexed (read_signature_line indexes lines directly);
2532    // display 1-indexed to match every other tool surface.
2533    parts.push(format!("L2: {}  [{}:{}]", l2_line, sym.file, sym.line + 1));
2534
2535    // L3: surprise
2536    if !body.is_empty() {
2537        let surprises = extract_surprise_from_body(body, skeleton_freq);
2538        if !surprises.is_empty() {
2539            parts.push(format!("L3: {}", surprises.join("; ")));
2540        }
2541    }
2542
2543    // Cross-refs (only in Full format, with ≥3 refs)
2544    if matches!(format, PackFormat::Full) && cross_refs.len() >= 2 {
2545        parts.push(format!("Cross-refs: {}", cross_refs.iter().take(5).cloned().collect::<Vec<_>>().join(", ")));
2546    }
2547
2548    // L4: co-change (editing guidance) — always present when we have cross-refs or type info
2549    let l4 = build_cochange_line(&sym.name, &sym.signature, body, cross_refs);
2550    if !l4.is_empty() {
2551        parts.push(format!("L4: {}", l4));
2552    }
2553
2554    parts.push(String::new());
2555    parts.join("\n")
2556}
2557
2558#[cfg(test)]
2559mod noise_tests {
2560    use super::is_noise_line;
2561
2562    /// Lines that must be filtered — each is a real noise class the S1 work
2563    /// targeted. If the filter is weakened these assertions fail.
2564    #[test]
2565    fn noise_classes_are_filtered() {
2566        let noise = [
2567            // blank / structural closers
2568            "",
2569            "}",
2570            "});",
2571            "};",
2572            "];",
2573            ")",
2574            // python assertions
2575            "self.assertEqual(request.status, 200)",
2576            "self.assertIsNotNone(request.body)",
2577            "self.assertTrue(request.valid)",
2578            // chained method calls of a collection
2579            "block_phrases.append(request.text)",
2580            "results.append(item)",
2581            // log macros
2582            "log::info!(\"starting engine\")",
2583            "tracing::debug!(\"rpm = {}\", self.rpm)",
2584            "println!(\"done\")",
2585            "dbg!(value);",
2586            // derive attributes
2587            "#[derive(Debug, Clone, PartialEq)]",
2588            "derive(Serialize, Deserialize)",
2589            // break / continue
2590            "break;",
2591            "continue;",
2592            // trivial returns
2593            "return None;",
2594            "return true;",
2595            "return false;",
2596            // discard bindings / option idioms
2597            "let _ = do_thing();",
2598            "value.ok()?;",
2599            "thing.expect(\"must exist\");",
2600            // simple bindings
2601            "let x = y;",
2602            "let mut count = 0;",
2603        ];
2604        for line in noise {
2605            assert!(
2606                is_noise_line(line),
2607                "expected to be filtered as noise: {:?}",
2608                line
2609            );
2610        }
2611    }
2612
2613    /// Lines that must survive — filtering them would lose real signal.
2614    #[test]
2615    fn signal_lines_are_preserved() {
2616        let signal = [
2617            "hash = hash.wrapping_mul(33).wrapping_add(byte);",
2618            "if total > threshold && !exhausted {",
2619            "match response_kind {",
2620            "let ratio = scored(matched, total) as f64;",
2621            "return Some(compute_fallback(input));",
2622            "buffer.extend_from_slice(&payload);",
2623            "channel.send(Message::Shutdown).await;",
2624        ];
2625        for line in signal {
2626            assert!(
2627                !is_noise_line(line),
2628                "expected to be preserved as signal: {:?}",
2629                line
2630            );
2631        }
2632    }
2633}
2634
2635#[cfg(test)]
2636mod v80_panic_tests {
2637    use super::*;
2638    #[test]
2639    fn manual_capture_sentinel_no_panic() {
2640        // Various edge shapes that put the needle near the end.
2641        for t in ["is_empty", "x is_empty", "// is_empty", "if self.is_empty", "is_empty\nreturn x;"] {
2642            let _ = manual_capture_sentinel(t); // must not panic
2643        }
2644    }
2645}