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