1use super::{
7 CrpMode, SymbolMap, append_compressed_hint, compressor, count_tokens, deps,
8 detect_project_root, entropy, protocol, resolve_auto_mode, signatures, symbol_map,
9};
10use lean_ctx::core::aggressiveness::AggressivenessProfile;
11
12#[cfg(test)]
13pub(crate) use super::fallback_banner::NO_COMPRESSION_BANNER_MIN_TOKENS;
14pub(crate) use super::fallback_banner::{AutoRequestGuard, no_compression_banner};
15
16fn monotonic_check(original: usize, compressed: usize) -> bool {
17 compressed < original
18}
19
20fn raw_fallback(
21 path: &str,
22 content: &str,
23 original_tokens: usize,
24 compressed: usize,
25) -> (String, usize) {
26 tracing::debug!(
27 "monotonic guard: {path} compressed {compressed} >= original {original_tokens}, using raw"
28 );
29 let mode =
30 lean_ctx::core::savings_footer::current_mode().unwrap_or_else(|| "compressed".into());
31 match no_compression_banner(&mode, original_tokens) {
32 Some(banner) => {
33 let out = format!("{banner}\n{content}");
34 let sent = count_tokens(&out);
35 (out, sent)
36 }
37 None => (content.to_string(), original_tokens),
38 }
39}
40
41#[derive(Clone, Copy, Default)]
45pub(crate) struct ReadTuning<'a> {
46 pub aggressiveness: Option<f64>,
50 pub protect: &'a [String],
55}
56
57impl<'a> ReadTuning<'a> {
58 pub(crate) fn resolve(explicit_aggressiveness: Option<f64>, protect: &'a [String]) -> Self {
62 Self {
63 aggressiveness: lean_ctx::core::aggressiveness::effective(explicit_aggressiveness),
64 protect,
65 }
66 }
67
68 pub(crate) fn auto_density_mode(self) -> Option<String> {
71 self.aggressiveness.map(|a| {
72 format!(
73 "density:{:.2}",
74 AggressivenessProfile::from_level(a).density_target
75 )
76 })
77 }
78}
79
80fn techniques_tag(techniques: &[String]) -> String {
85 if techniques.is_empty() {
86 String::new()
87 } else {
88 format!(" [{}]", techniques.join(", "))
89 }
90}
91
92fn health_annotations(content: &str, ext: &str) -> std::collections::HashMap<String, String> {
97 let cfg = lean_ctx::core::config::Config::load();
98 if !cfg.code_health.annotate_reads {
99 return std::collections::HashMap::new();
100 }
101 let annotations = lean_ctx::core::code_health::annotate::annotations_for_file(
102 content,
103 ext,
104 cfg.code_health.cognitive_threshold,
105 );
106 lean_ctx::core::code_health::annotate::by_name(&annotations)
107}
108
109pub(crate) fn format_full_output(
110 file_ref: &str,
111 short: &str,
112 ext: &str,
113 content: &str,
114 original_tokens: usize,
115 line_count: usize,
116 _task: Option<&str>,
117) -> (String, usize) {
118 let _mode_guard = lean_ctx::core::savings_footer::ModeGuard::new("full");
119 let tokens = original_tokens;
120 let metadata = build_header(file_ref, short, ext, content, line_count, true);
121
122 let output = format!("{metadata}\n{content}");
123 let sent = count_tokens(&output);
124 (protocol::append_savings(&output, tokens, sent), sent)
125}
126
127pub(crate) fn format_full_compact_output(content: &str) -> (String, usize) {
134 let _mode_guard = lean_ctx::core::savings_footer::ModeGuard::new("full-compact");
135 let output: String = content
136 .lines()
137 .map(str::trim_end)
138 .collect::<Vec<_>>()
139 .join("\n");
140 let sent = count_tokens(&output);
141 (output, sent)
142}
143
144pub(crate) fn format_anchored_output(
155 file_ref: &str,
156 short: &str,
157 content: &str,
158 line_count: usize,
159) -> (String, usize) {
160 format_anchored_output_window(file_ref, short, content, line_count, None)
161}
162
163pub(crate) fn format_anchored_output_window(
170 file_ref: &str,
171 short: &str,
172 body: &str,
173 line_count: usize,
174 window: Option<(usize, usize)>,
175) -> (String, usize) {
176 let _mode_guard = lean_ctx::core::savings_footer::ModeGuard::new("anchored");
177 let (start_line, range_suffix) = match window {
178 Some((start, end)) => (start, format!(" lines:{start}-{end}")),
179 None => (1, String::new()),
180 };
181 let header = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
182 format!(
183 "{file_ref}={short} {line_count}L{range_suffix} [anchored: N:hh|line → edit via ctx_patch]"
184 )
185 } else {
186 format!("{short} {line_count}L{range_suffix} [anchored: N:hh|line → edit via ctx_patch]")
187 };
188 let annotated = lean_ctx::core::anchor::annotate(body, start_line);
189 let output = format!("{header}\n{annotated}");
190 let sent = count_tokens(&output);
191 (output, sent)
192}
193
194pub(crate) fn build_header(
195 file_ref: &str,
196 short: &str,
197 ext: &str,
198 content: &str,
199 line_count: usize,
200 include_deps: bool,
201) -> String {
202 let mut header = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
203 format!("{file_ref}={short} {line_count}L")
204 } else {
205 format!("{short} {line_count}L")
206 };
207
208 if include_deps {
209 let dep_info = deps::extract_deps(content, ext);
210 if !dep_info.imports.is_empty() {
211 let imports_str: Vec<&str> = dep_info
212 .imports
213 .iter()
214 .take(8)
215 .map(std::string::String::as_str)
216 .collect();
217 header.push_str(&format!("\n deps {}", imports_str.join(",")));
218 }
219 if !dep_info.exports.is_empty() {
220 let exports_str: Vec<&str> = dep_info
221 .exports
222 .iter()
223 .take(8)
224 .map(std::string::String::as_str)
225 .collect();
226 header.push_str(&format!("\n exports {}", exports_str.join(",")));
227 }
228 }
229
230 header
231}
232
233#[allow(clippy::too_many_arguments)]
234pub fn process_mode(
235 content: &str,
236 mode: &str,
237 file_ref: &str,
238 short: &str,
239 ext: &str,
240 original_tokens: usize,
241 crp_mode: CrpMode,
242 file_path: &str,
243 task: Option<&str>,
244) -> (String, usize) {
245 process_mode_tuned(
246 content,
247 mode,
248 file_ref,
249 short,
250 ext,
251 original_tokens,
252 crp_mode,
253 file_path,
254 task,
255 ReadTuning::default(),
256 )
257}
258
259pub fn register_conformance_renderer() {
263 lean_ctx::core::conformance::register_read_mode_renderer(process_mode);
264}
265
266#[allow(clippy::too_many_arguments)]
270pub(crate) fn process_mode_tuned(
271 content: &str,
272 mode: &str,
273 file_ref: &str,
274 short: &str,
275 ext: &str,
276 original_tokens: usize,
277 crp_mode: CrpMode,
278 file_path: &str,
279 task: Option<&str>,
280 tuning: ReadTuning<'_>,
281) -> (String, usize) {
282 let _mode_guard = lean_ctx::core::savings_footer::ModeGuard::new(mode);
283 let line_count = content.lines().count();
284 let ctx = RenderCtx {
285 file_ref,
286 short,
287 ext,
288 file_path,
289 original_tokens,
290 crp_mode,
291 line_count,
292 task,
293 };
294
295 match mode {
296 "raw" => {
297 let sent = count_tokens(content);
298 (content.to_string(), sent)
299 }
300 "auto" => {
301 let chosen = tuning.auto_density_mode().unwrap_or_else(|| {
305 let lc = content.lines().count();
306 resolve_auto_mode(None, file_path, original_tokens, Some(lc), task)
307 });
308 process_mode_tuned(
309 content,
310 &chosen,
311 file_ref,
312 short,
313 ext,
314 original_tokens,
315 crp_mode,
316 file_path,
317 task,
318 tuning,
319 )
320 }
321 "full" => format_full_output(
322 file_ref,
323 short,
324 ext,
325 content,
326 original_tokens,
327 line_count,
328 task,
329 ),
330 "full-compact" => format_full_compact_output(content),
331 "anchored" => format_anchored_output(file_ref, short, content, line_count),
332 mode if mode.starts_with("anchored:") => {
333 let range_str = &mode[9..];
334 let (start, end) = parse_anchored_range(range_str, line_count);
335 let (body, start, end) = slice_window(content, start, end);
336 format_anchored_output_window(file_ref, short, &body, line_count, Some((start, end)))
337 }
338 "signatures" => render_signatures(content, ctx),
339 "map" | "cognitive" | "mdl" => render_map(content, ctx),
342 "aggressive" => render_aggressive(content, ctx),
343 "entropy" => render_entropy(content, ctx, &tuning),
344 "task" => render_task_mode(content, ctx, &tuning),
345 "reference" => {
346 let tok = count_tokens(content);
347 let output = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
348 format!("{file_ref}={short}: {line_count} lines, {tok} tok ({ext})")
349 } else {
350 format!("{short}: {line_count} lines, {tok} tok ({ext})")
351 };
352 let sent = count_tokens(&output);
353 let savings = protocol::format_savings(original_tokens, sent);
354 (format!("{output}\n{savings}"), sent)
355 }
356 mode if mode.starts_with("lines:") => {
357 let range_str = &mode[6..];
358 let extracted = extract_line_range(content, range_str);
359 let header = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
360 format!("{file_ref}={short} {line_count}L lines:{range_str}")
361 } else {
362 format!("{short} {line_count}L lines:{range_str}")
363 };
364 let multi_hint = if range_str.contains(',') {
365 LINES_COMMA_HINT
366 } else {
367 ""
368 };
369 let sent = count_tokens(&extracted);
370 let savings = protocol::format_savings(original_tokens, sent);
371 (
372 format!("{header}\n{extracted}{multi_hint}\n{savings}"),
373 sent,
374 )
375 }
376 mode if mode.starts_with("density:") => {
377 let aggr_target = tuning
381 .aggressiveness
382 .map(|a| AggressivenessProfile::from_level(a).density_target);
383 let target: f64 = mode[8..].parse().ok().or(aggr_target).unwrap_or(0.5);
384 let result = entropy::entropy_compress_to_density_for(content, target, ext);
385 let (guarded_output, _q) =
389 lean_ctx::core::quality::guard_density(content, &result.output, ext, target);
390 let guarded_tokens = count_tokens(&guarded_output);
391 let actual = if result.original_tokens > 0 {
392 guarded_tokens as f64 / result.original_tokens as f64
393 } else {
394 0.0
395 };
396 let techs = techniques_tag(&result.techniques);
397 let target_clamped = target.clamp(0.05, 1.0);
398 let header = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
399 format!(
400 "{file_ref}={short} {line_count}L density target={target_clamped:.2} actual={actual:.2}{techs}"
401 )
402 } else {
403 format!(
404 "{short} {line_count}L density target={target_clamped:.2} actual={actual:.2}{techs}"
405 )
406 };
407 let output = format!("{header}\n{guarded_output}");
408 finish_lossy_view(&output, content, original_tokens, file_path)
409 }
410 "diff" => {
417 let msg = format!(
418 "{short}: mode=diff renders against the session cache and cannot be produced here. \
419 Read the file once with mode=full, then request mode=diff on the re-read."
420 );
421 let sent = count_tokens(&msg);
422 (msg, sent)
423 }
424 unknown => {
425 let header = build_header(file_ref, short, ext, content, line_count, true);
426 let out = format!(
427 "[WARNING: unknown mode '{unknown}', falling back to full — valid modes: {}]\n{header}\n{content}",
428 lean_ctx::core::mcp_manifest::READ_MODES.join(", ")
429 );
430 let sent = count_tokens(&out);
431 (out, sent)
432 }
433 }
434}
435
436#[cfg(not(feature = "tree-sitter"))]
444pub(crate) fn task_relevant_body(
445 _content: &str,
446 _file_path: &str,
447 _ext: &str,
448 _task: Option<&str>,
449) -> Option<String> {
450 None
451}
452
453#[cfg(feature = "tree-sitter")]
454pub(crate) fn task_relevant_body(
455 content: &str,
456 file_path: &str,
457 ext: &str,
458 task: Option<&str>,
459) -> Option<String> {
460 const MAX_BODY_LINES: usize = 80;
461
462 let task = task.map(str::trim).filter(|t| !t.is_empty())?;
463 let (_files, keywords) = lean_ctx::core::task_relevance::parse_task_hints(task);
464 if keywords.is_empty() {
465 return None;
466 }
467 let kw_lower: Vec<String> = keywords.iter().map(|k| k.to_lowercase()).collect();
468
469 let chunks = lean_ctx::core::chunks_ts::extract_chunks_ts(file_path, content, ext)?;
470
471 let mut best_idx: Option<usize> = None;
473 let mut best_score = 0u8;
474 for (i, ch) in chunks.iter().enumerate() {
475 if ch.symbol_name.is_empty() {
476 continue;
477 }
478 let name_l = ch.symbol_name.to_lowercase();
479 let substr = kw_lower
480 .iter()
481 .any(|k| k.len() >= 3 && (name_l.contains(k.as_str()) || k.contains(name_l.as_str())));
482 let score = if kw_lower.contains(&name_l) {
483 2
484 } else {
485 u8::from(substr)
486 };
487 if score > best_score {
488 best_score = score;
489 best_idx = Some(i);
490 }
491 }
492
493 let ch = &chunks[best_idx?];
494 let body_lines: Vec<&str> = ch.content.lines().collect();
495 let total = body_lines.len();
496 let shown = total.min(MAX_BODY_LINES);
497 let body: String = body_lines[..shown]
498 .iter()
499 .enumerate()
500 .map(|(i, l)| format!("{:>4}|{l}", ch.start_line + i))
501 .collect::<Vec<_>>()
502 .join("\n");
503 let truncated = if shown < total {
504 format!(
505 "\n … +{} lines — ctx_read(mode=\"lines:{}-{}\")",
506 total - shown,
507 ch.start_line + shown,
508 ch.end_line
509 )
510 } else {
511 String::new()
512 };
513 Some(format!(
514 " ▸ body {} L{}-{}:\n{body}{truncated}",
515 ch.symbol_name, ch.start_line, ch.end_line
516 ))
517}
518
519pub const LINES_COMMA_HINT: &str = "\n[lines: comma = multi-select (e.g. 5,10-20 picks line 5 and lines 10-20); use N-M for one span]";
523
524pub fn no_structure_marker(ext: &str) -> String {
528 format!(
529 "\n [no extractable structure for .{ext} — header only; use mode=\"lines:N-M\" or \"full\"]"
530 )
531}
532
533pub fn extract_line_range(content: &str, range_str: &str) -> String {
534 let lines: Vec<&str> = content.lines().collect();
535 let total = lines.len();
536 let mut selected = Vec::new();
537
538 for part in range_str.split(',') {
539 let part = part.trim();
540 if let Some(count_s) = part.strip_prefix('-') {
541 if let Ok(count) = count_s.trim().parse::<usize>()
545 && count >= 1
546 {
547 let start = total.saturating_sub(count) + 1;
548 for i in start..=total {
549 selected.push(format!("{i:>4}| {}", lines[i - 1]));
550 }
551 }
552 } else if let Some((start_s, end_s)) = part.split_once('-') {
553 let start = start_s.trim().parse::<usize>().unwrap_or(1).max(1);
554 let end = end_s.trim().parse::<usize>().unwrap_or(total).min(total);
555 for i in start..=end {
556 if i >= 1 && i <= total {
557 selected.push(format!("{i:>4}| {}", lines[i - 1]));
558 }
559 }
560 } else if let Ok(n) = part.parse::<usize>()
561 && n >= 1
562 && n <= total
563 {
564 selected.push(format!("{n:>4}| {}", lines[n - 1]));
565 }
566 }
567
568 if selected.is_empty() {
569 "No lines matched the range.".to_string()
570 } else {
571 selected.join("\n")
572 }
573}
574
575fn parse_anchored_range(range_str: &str, total: usize) -> (usize, usize) {
580 if let Some((s, e)) = range_str.split_once('-') {
581 let start = s.trim().parse::<usize>().unwrap_or(1).max(1);
582 let end = e.trim().parse::<usize>().unwrap_or(total).min(total);
583 (start, end)
584 } else {
585 let start = range_str.trim().parse::<usize>().unwrap_or(1).max(1);
586 (start, total)
587 }
588}
589
590fn slice_window(content: &str, start: usize, end: usize) -> (String, usize, usize) {
594 let lines: Vec<&str> = content.lines().collect();
595 let total = lines.len();
596 let start = start.max(1).min(total.max(1));
597 let end = end.min(total);
598 let body = if total > 0 && end >= start {
599 lines[start - 1..end].join("\n")
600 } else {
601 String::new()
602 };
603 (body, start, end)
604}
605
606#[cfg(test)]
607mod render_tests {
608 use super::techniques_tag;
609
610 #[test]
613 fn conformance_suite_passes_with_the_read_renderer() {
614 super::register_conformance_renderer();
615 let card = lean_ctx::core::conformance::run();
616 assert!(
617 card.all_passed(),
618 "conformance failures: {:?}",
619 card.failures()
620 );
621 assert!(
622 card.checks.iter().any(|c| c.category == "accuracy"
623 && c.name.starts_with("read_mode_keeps_symbols:")),
624 "the read-mode accuracy checks ran"
625 );
626 }
627
628 #[test]
629 fn techniques_tag_omits_empty_brackets() {
630 assert_eq!(techniques_tag(&[]), "");
633 }
634
635 #[test]
636 fn techniques_tag_wraps_nonempty_with_leading_space() {
637 assert_eq!(
638 techniques_tag(&["⊘ 3 low-entropy lines".to_string(), "⊘ 2 dups".to_string()]),
639 " [⊘ 3 low-entropy lines, ⊘ 2 dups]"
640 );
641 }
642
643 #[test]
644 fn techniques_tag_single() {
645 assert_eq!(
646 techniques_tag(&["density target=0.40".to_string()]),
647 " [density target=0.40]"
648 );
649 }
650
651 #[test]
656 fn diff_mode_never_falls_through_to_the_unknown_mode_dump() {
657 let content = "fn a() {}\nfn b() {}\nfn c() {}\n";
658 let (out, _) = super::process_mode_tuned(
659 content,
660 "diff",
661 "",
662 "sample.rs",
663 "rs",
664 32,
665 crate::tools::CrpMode::Off,
666 "sample.rs",
667 None,
668 super::ReadTuning {
669 aggressiveness: None,
670 protect: &[],
671 },
672 );
673 assert!(
674 !out.contains("unknown mode"),
675 "diff is a documented mode, not an unknown one: {out}"
676 );
677 assert!(
678 !out.contains("fn b()"),
679 "the fallback must not dump the file the caller did not ask for: {out}"
680 );
681 assert!(
682 out.contains("mode=full"),
683 "it must name the way forward: {out}"
684 );
685 }
686
687 #[test]
691 fn unknown_mode_warning_names_the_valid_modes() {
692 let (out, _) = super::process_mode_tuned(
693 "fn a() {}\n",
694 "definitely-not-a-mode",
695 "",
696 "sample.rs",
697 "rs",
698 8,
699 crate::tools::CrpMode::Off,
700 "sample.rs",
701 None,
702 super::ReadTuning {
703 aggressiveness: None,
704 protect: &[],
705 },
706 );
707 assert!(
708 out.contains("unknown mode 'definitely-not-a-mode'"),
709 "{out}"
710 );
711 for mode in lean_ctx::core::mcp_manifest::READ_MODES {
712 assert!(out.contains(mode), "warning must list `{mode}`: {out}");
713 }
714 }
715}
716
717#[derive(Clone, Copy)]
721struct RenderCtx<'a> {
722 file_ref: &'a str,
723 short: &'a str,
724 ext: &'a str,
725 file_path: &'a str,
726 original_tokens: usize,
727 crp_mode: CrpMode,
728 line_count: usize,
729 task: Option<&'a str>,
730}
731
732fn render_signatures(content: &str, ctx: RenderCtx<'_>) -> (String, usize) {
733 let RenderCtx {
734 file_ref,
735 short,
736 ext,
737 file_path,
738 original_tokens,
739 crp_mode,
740 line_count,
741 task,
742 } = ctx;
743 let sigs = signatures::extract_signatures(content, ext);
744 let dep_info = deps::extract_deps(content, ext);
745
746 let mut output = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
747 format!("{file_ref}={short} {line_count}L")
748 } else {
749 format!("{short} {line_count}L")
750 };
751 if !dep_info.imports.is_empty() {
752 let imports_str: Vec<&str> = dep_info
753 .imports
754 .iter()
755 .take(8)
756 .map(std::string::String::as_str)
757 .collect();
758 output.push_str(&format!("\n deps {}", imports_str.join(",")));
759 }
760 if crp_mode.is_tdd() {
763 let refs: Vec<&signatures::Signature> = sigs.iter().collect();
764 let legend = signatures::tdd_legend(&refs);
765 if !legend.is_empty() {
766 output.push('\n');
767 output.push_str(&legend);
768 }
769 }
770 let health = health_annotations(content, ext);
771 let refs: Vec<&signatures::Signature> = sigs.iter().collect();
772 let (lines, budget_note) =
773 super::outline_budget::outline_lines(&refs, crp_mode.is_tdd(), &health);
774 for line in lines.iter().chain(budget_note.iter()) {
775 output.push('\n');
776 output.push_str(line);
777 }
778 if sigs.is_empty() && dep_info.imports.is_empty() {
781 output.push_str(&no_structure_marker(ext));
782 }
783 if let Some(body) = task_relevant_body(content, file_path, ext, task) {
784 output.push('\n');
785 output.push_str(&body);
786 }
787 if lean_ctx::core::profiles::active_profile()
790 .output_hints
791 .compressed_hint()
792 && !sigs.is_empty()
793 {
794 output.push_str(&format!(
795 "\n ↳ expand a symbol: ctx_read(\"{file_path}\", mode=\"lines:N-M\") using the spans above"
796 ));
797 output.push_str(&format!("\n {}", lean_ctx::core::handle::USAGE_HINT));
799 }
800 let sent = count_tokens(&output);
801 (
802 append_compressed_hint(
803 &protocol::append_savings(&output, original_tokens, sent),
804 file_path,
805 ),
806 sent,
807 )
808}
809
810fn render_map(content: &str, ctx: RenderCtx<'_>) -> (String, usize) {
811 let RenderCtx {
812 file_ref,
813 short,
814 ext,
815 file_path,
816 original_tokens,
817 crp_mode,
818 line_count,
819 task,
820 } = ctx;
821 if ext == "php"
822 && let Some(php_map) = lean_ctx::core::patterns::php::compress_php_map(content, short)
823 {
824 let output = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
825 format!("{file_ref}={short} {line_count}L\n{php_map}")
826 } else {
827 format!("{short} {line_count}L\n{php_map}")
828 };
829 let sent = count_tokens(&output);
830 let output = protocol::append_savings(&output, original_tokens, sent);
831 return (append_compressed_hint(&output, file_path), sent);
832 }
833
834 let structured = match ext {
835 "md" | "mdx" | "rst" => lean_ctx::core::structured_read::extract_markdown_outline(content),
836 "json" => lean_ctx::core::structured_read::extract_json_structure(content),
837 "yaml" | "yml" => lean_ctx::core::structured_read::extract_yaml_structure(content),
838 "toml" => lean_ctx::core::structured_read::extract_toml_structure(content),
839 _ if file_path.to_lowercase().ends_with(".lock")
840 || file_path.to_lowercase().ends_with("go.sum") =>
841 {
842 lean_ctx::core::structured_read::extract_lock_summary(content, file_path)
843 }
844 _ => String::new(),
845 };
846
847 if !structured.is_empty() {
848 let mut output = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
849 format!("{file_ref}={short} {line_count}L\n{structured}")
850 } else {
851 format!("{short} {line_count}L\n{structured}")
852 };
853 let sent = count_tokens(&output);
854 output = protocol::append_savings(&output, original_tokens, sent);
855 return (append_compressed_hint(&output, file_path), sent);
856 }
857
858 let sigs = signatures::extract_signatures(content, ext);
859 let dep_info = deps::extract_deps(content, ext);
860
861 let mut output = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
862 format!("{file_ref}={short} {line_count}L")
863 } else {
864 format!("{short} {line_count}L")
865 };
866
867 if !dep_info.imports.is_empty() {
868 output.push_str("\n deps: ");
869 output.push_str(&dep_info.imports.join(", "));
870 }
871
872 let key_sigs: Vec<&signatures::Signature> = sigs
873 .iter()
874 .filter(|s| s.is_exported || s.indent == 0)
875 .collect();
876
877 let extra_exports = signatures::exports_not_in_signatures(&dep_info.exports, &key_sigs);
880 if !extra_exports.is_empty() {
881 output.push_str("\n exports: ");
882 output.push_str(&extra_exports.join(", "));
883 }
884
885 if !key_sigs.is_empty() {
886 output.push_str("\n API:");
887 if crp_mode.is_tdd() {
889 let legend = signatures::tdd_legend(&key_sigs);
890 if !legend.is_empty() {
891 output.push_str(&format!(" {legend}"));
892 }
893 }
894 let health = health_annotations(content, ext);
895 let (lines, budget_note) =
896 super::outline_budget::outline_lines(&key_sigs, crp_mode.is_tdd(), &health);
897 for line in &lines {
898 output.push_str("\n ");
899 output.push_str(line);
900 }
901 if let Some(note) = budget_note {
902 output.push_str("\n ");
903 output.push_str(¬e);
904 }
905 }
906
907 if key_sigs.is_empty() && dep_info.imports.is_empty() && extra_exports.is_empty() {
911 output.push_str(&no_structure_marker(ext));
912 }
913
914 if let Some(body) = task_relevant_body(content, file_path, ext, task) {
915 output.push('\n');
916 output.push_str(&body);
917 }
918 if lean_ctx::core::profiles::active_profile()
920 .output_hints
921 .compressed_hint()
922 && !key_sigs.is_empty()
923 {
924 output.push_str(&format!("\n {}", lean_ctx::core::handle::USAGE_HINT));
925 }
926
927 let sent = count_tokens(&output);
928 (
929 append_compressed_hint(
930 &protocol::append_savings(&output, original_tokens, sent),
931 file_path,
932 ),
933 sent,
934 )
935}
936
937fn render_aggressive(content: &str, ctx: RenderCtx<'_>) -> (String, usize) {
938 let RenderCtx {
939 file_ref,
940 short,
941 ext,
942 file_path,
943 original_tokens,
944 line_count,
945 ..
946 } = ctx;
947 if ext == "json"
953 && let Some(crushed) = lean_ctx::core::json_crush::crush_text_if_beneficial(content)
954 {
955 let header = build_header(file_ref, short, ext, content, line_count, true);
956 let body = format!("{header}\n{crushed}");
957 let sent = count_tokens(&body);
958 if monotonic_check(original_tokens, sent) {
959 let savings = protocol::format_savings(original_tokens, sent);
960 return (
961 append_compressed_hint(&format!("{body}\n{savings}"), file_path),
962 sent,
963 );
964 }
965 return raw_fallback(file_path, content, original_tokens, sent);
966 }
967
968 if let Some(delim) = compressor::tabular_delimiter(Some(ext))
972 && let Some(crushed) =
973 lean_ctx::core::tabular_crush::crush_text_if_beneficial(content, delim)
974 {
975 let header = build_header(file_ref, short, ext, content, line_count, true);
976 let body = format!("{header}\n{crushed}");
977 let sent = count_tokens(&body);
978 if monotonic_check(original_tokens, sent) {
979 let savings = protocol::format_savings(original_tokens, sent);
980 return (
981 append_compressed_hint(&format!("{body}\n{savings}"), file_path),
982 sent,
983 );
984 }
985 return raw_fallback(file_path, content, original_tokens, sent);
986 }
987
988 if compressor::is_yaml_ext(Some(ext))
993 && let Some(crushed) = lean_ctx::core::yaml_crush::crush_text_if_beneficial(content)
994 {
995 let header = build_header(file_ref, short, ext, content, line_count, true);
996 let body = format!("{header}\n{crushed}");
997 let sent = count_tokens(&body);
998 if monotonic_check(original_tokens, sent) {
999 let savings = protocol::format_savings(original_tokens, sent);
1000 return (
1001 append_compressed_hint(&format!("{body}\n{savings}"), file_path),
1002 sent,
1003 );
1004 }
1005 return raw_fallback(file_path, content, original_tokens, sent);
1006 }
1007
1008 #[cfg(feature = "tree-sitter")]
1009 let ast_pruned = lean_ctx::core::signatures_ts::ast_prune(content, ext);
1010 #[cfg(not(feature = "tree-sitter"))]
1011 let ast_pruned: Option<String> = None;
1012
1013 let base = ast_pruned.as_deref().unwrap_or(content);
1014
1015 let session_intent =
1016 lean_ctx::core::session::caller::with(|s| s.active_structured_intent.clone()).flatten();
1017 let raw = if let Some(ref intent) = session_intent {
1018 compressor::task_aware_compress(base, Some(ext), intent)
1019 } else {
1020 compressor::aggressive_compress(base, Some(ext))
1021 };
1022 let compressed = compressor::safeguard_ratio(content, &raw);
1023 let header = build_header(file_ref, short, ext, content, line_count, true);
1024
1025 let mut sym = SymbolMap::new();
1026 let idents = symbol_map::extract_identifiers(&compressed, &[ext]);
1027 for ident in &idents {
1028 sym.register(ident);
1029 }
1030
1031 if symbol_map::substitution_enabled() && sym.len() >= 3 {
1032 let sym_table = sym.format_table();
1033 let sym_applied = sym.apply(&compressed);
1034 let orig_tok = count_tokens(&compressed);
1035 let comp_tok = count_tokens(&sym_applied) + count_tokens(&sym_table);
1036 let net = orig_tok.saturating_sub(comp_tok);
1037 if orig_tok > 0 && net * 100 / orig_tok >= 5 {
1038 if !monotonic_check(original_tokens, comp_tok) {
1039 return raw_fallback(file_path, content, original_tokens, comp_tok);
1040 }
1041 let savings = protocol::format_savings(original_tokens, comp_tok);
1042 return (
1043 append_compressed_hint(
1044 &format!("{header}\n{sym_applied}{sym_table}\n{savings}"),
1045 file_path,
1046 ),
1047 comp_tok,
1048 );
1049 }
1050 if !monotonic_check(original_tokens, orig_tok) {
1051 return raw_fallback(file_path, content, original_tokens, orig_tok);
1052 }
1053 let savings = protocol::format_savings(original_tokens, orig_tok);
1054 return (
1055 append_compressed_hint(&format!("{header}\n{compressed}\n{savings}"), file_path),
1056 orig_tok,
1057 );
1058 }
1059
1060 let sent = count_tokens(&compressed);
1061 if !monotonic_check(original_tokens, sent) {
1062 return raw_fallback(file_path, content, original_tokens, sent);
1063 }
1064 let savings = protocol::format_savings(original_tokens, sent);
1065 (
1066 append_compressed_hint(&format!("{header}\n{compressed}\n{savings}"), file_path),
1067 sent,
1068 )
1069}
1070
1071fn render_entropy(content: &str, ctx: RenderCtx<'_>, tuning: &ReadTuning<'_>) -> (String, usize) {
1072 let RenderCtx {
1073 file_ref,
1074 short,
1075 ext,
1076 file_path,
1077 original_tokens,
1078 line_count,
1079 task,
1080 ..
1081 } = ctx;
1082 let task_kws: Vec<String> = task
1085 .filter(|t| !t.trim().is_empty())
1086 .map(|t| lean_ctx::core::task_relevance::parse_task_hints(t).1)
1087 .filter(|kws| !kws.is_empty())
1088 .or_else(|| {
1089 lean_ctx::core::session::caller::with(|session| {
1091 if let Some(intent) = &session.active_structured_intent
1092 && !intent.keywords.is_empty()
1093 {
1094 return Some(intent.keywords.clone());
1095 }
1096 let q = session.last_semantic_query.as_deref()?;
1097 let kws = lean_ctx::core::task_relevance::parse_task_hints(q).1;
1098 (!kws.is_empty()).then_some(kws)
1099 })
1100 .flatten()
1101 })
1102 .unwrap_or_default();
1103 let result = match (task_kws.is_empty(), tuning.aggressiveness) {
1104 (true, Some(a)) => entropy::entropy_compress_with_threshold(
1108 content,
1109 file_path,
1110 AggressivenessProfile::from_level(a).bpe_entropy,
1111 tuning.protect,
1112 ),
1113 (true, None) => entropy::entropy_compress_adaptive(content, file_path, tuning.protect),
1114 (false, _) => entropy::entropy_compress_task_conditioned(
1115 content,
1116 file_path,
1117 &task_kws,
1118 tuning.protect,
1119 ),
1120 };
1121 let avg_h = entropy::analyze_entropy(content).avg_entropy;
1122 let header = build_header(file_ref, short, ext, content, line_count, false);
1123 let output = format!(
1124 "{header} H̄={avg_h:.1}{}\n{}",
1125 techniques_tag(&result.techniques),
1126 result.output
1127 );
1128 finish_lossy_view(&output, content, original_tokens, file_path)
1129}
1130
1131fn finish_lossy_view(
1136 output: &str,
1137 content: &str,
1138 original_tokens: usize,
1139 file_path: &str,
1140) -> (String, usize) {
1141 let sent = count_tokens(output);
1142 if !monotonic_check(original_tokens, sent) {
1143 return raw_fallback(file_path, content, original_tokens, sent);
1144 }
1145 let savings = protocol::format_savings(original_tokens, sent);
1146 let delivered = append_compressed_hint(&format!("{output}\n{savings}"), file_path);
1147 if !monotonic_check(original_tokens, count_tokens(&delivered)) {
1148 return raw_fallback(file_path, content, original_tokens, sent);
1149 }
1150 (delivered, sent)
1151}
1152
1153fn render_task_selection(keywords: &[String], selected: &[(usize, &str)]) -> String {
1161 let mut out = format!("[task: {}]", keywords.join(", "));
1162 let mut prev_line: Option<usize> = None;
1163 for (idx, line) in selected {
1164 let lineno = idx + 1;
1165 if let Some(prev) = prev_line
1166 && lineno > prev + 1
1167 {
1168 let skipped = lineno - prev - 1;
1169 out.push_str(&format!("\n… {skipped}L"));
1170 }
1171 out.push_str(&format!("\n{lineno}|{line}"));
1172 prev_line = Some(lineno);
1173 }
1174 out
1175}
1176
1177fn render_task_mode(content: &str, ctx: RenderCtx<'_>, tuning: &ReadTuning<'_>) -> (String, usize) {
1178 let RenderCtx {
1179 file_ref,
1180 short,
1181 ext,
1182 file_path,
1183 original_tokens,
1184 line_count,
1185 task,
1186 ..
1187 } = ctx;
1188 let task_str = task.unwrap_or("");
1189 if task_str.is_empty() {
1190 let header = build_header(file_ref, short, ext, content, line_count, true);
1191 let out = format!("{header}\n{content}\n[task mode: no task set — returned full]");
1192 let sent = count_tokens(&out);
1193 return (out, sent);
1194 }
1195 let (_files, keywords) = lean_ctx::core::task_relevance::parse_task_hints(task_str);
1196 if keywords.is_empty() {
1197 let header = build_header(file_ref, short, ext, content, line_count, true);
1198 let out =
1199 format!("{header}\n{content}\n[task mode: no keywords extracted — returned full]");
1200 let sent = count_tokens(&out);
1201 return (out, sent);
1202 }
1203 const TASK_MIN_LINES: usize = 250;
1208 if line_count < TASK_MIN_LINES {
1209 let header = build_header(file_ref, short, ext, content, line_count, true);
1210 let out = format!(
1211 "{header}\n{content}\n[task mode: file below {TASK_MIN_LINES}L threshold — returned full]"
1212 );
1213 let sent = count_tokens(&out);
1214 return (out, sent);
1215 }
1216 let ib_budget = tuning.aggressiveness.map_or(0.3, |a| {
1219 AggressivenessProfile::from_level(a).ib_budget_ratio
1220 });
1221 let is_markdown = matches!(ext, "md" | "markdown" | "mdx" | "rst");
1222 let filtered = if is_markdown {
1223 lean_ctx::core::task_relevance::information_bottleneck_filter_with_headers(
1224 content,
1225 &keywords,
1226 ib_budget,
1227 tuning.protect,
1228 true,
1229 )
1230 } else {
1231 let selected = lean_ctx::core::task_relevance::ib_select(
1234 content,
1235 &keywords,
1236 ib_budget,
1237 None,
1238 tuning.protect,
1239 );
1240 render_task_selection(&keywords, &selected)
1241 };
1242 let filtered_lines = filtered.lines().count();
1243 let header = if lean_ctx::core::protocol::meta_visible() && !file_ref.is_empty() {
1244 format!("{file_ref}={short} {line_count}L [task-filtered: {line_count}→{filtered_lines}]")
1245 } else {
1246 format!("{short} {line_count}L [task-filtered: {line_count}→{filtered_lines}]")
1247 };
1248 let graph_ctx = if lean_ctx::core::profiles::active_profile()
1249 .output_hints
1250 .graph_context_block()
1251 {
1252 let project_root = detect_project_root(file_path);
1253 lean_ctx::core::graph_context::build_graph_context(
1254 file_path,
1255 &project_root,
1256 Some(lean_ctx::core::graph_context::GraphContextOptions::default()),
1257 )
1258 .map(|c| lean_ctx::core::graph_context::format_graph_context(&c))
1259 .unwrap_or_default()
1260 } else {
1261 String::new()
1262 };
1263
1264 let sent = count_tokens(&filtered) + count_tokens(&header) + count_tokens(&graph_ctx);
1265 if !monotonic_check(original_tokens, sent) {
1266 return raw_fallback(file_path, content, original_tokens, sent);
1267 }
1268 let savings = protocol::format_savings(original_tokens, sent);
1269 (
1270 append_compressed_hint(
1271 &format!("{header}\n{filtered}{graph_ctx}\n{savings}"),
1272 file_path,
1273 ),
1274 sent,
1275 )
1276}