1#![doc = include_str!("../README.md")]
2#![deny(
3 clippy::unwrap_used,
4 clippy::expect_used,
5 clippy::print_stdout,
6 clippy::print_stderr
7)]
8#![allow(
9 clippy::multiple_crate_versions,
10 reason = "Dependency graph pulls distinct versions (e.g., yaml-rust2)."
11)]
12#![cfg_attr(
13 test,
14 allow(
15 clippy::unwrap_used,
16 clippy::expect_used,
17 reason = "tests may use unwrap/expect for brevity"
18 )
19)]
20
21use anyhow::Result;
22
23pub mod budget;
24mod debug;
25mod grep;
26mod ingest;
27mod order;
28mod pruner;
29mod serialization;
30mod utils;
31pub use grep::{
32 GrepConfig, GrepPatterns, GrepShow, build_grep_config,
33 build_grep_config_from_patterns, combine_patterns,
34};
35pub use ingest::fileset::{FilesetInput, FilesetInputKind};
36pub use ingest::format::Format;
37pub use order::types::{ArrayBias, ArraySamplerStrategy};
38pub use order::{
39 DEFAULT_SAFETY_CAP, NodeId, NodeKind, PriorityConfig, PriorityOrder,
40 RankedNode, build_order,
41};
42pub use utils::extensions;
43pub use utils::templates::map_json_template_for_style;
44
45pub use pruner::budget::find_largest_render_under_budgets;
46pub use prunist::{Budget, BudgetKind, Budgets};
47pub use serialization::color::resolve_color_enabled;
48pub use serialization::types::{
49 ColorMode, ColorStrategy, OutputTemplate, RenderConfig, Style,
50};
51
52#[derive(Debug, Clone, Copy)]
53pub struct MatchSummary {
54 pub shown: usize,
55 pub hidden: usize,
56}
57
58#[derive(Debug)]
59pub struct RenderOutput {
60 pub text: String,
61 pub warnings: Vec<String>,
62 pub match_summary: Option<MatchSummary>,
63}
64
65#[derive(Copy, Clone, Debug)]
66pub enum TextMode {
67 Plain,
68 CodeLike,
69}
70
71pub enum InputKind {
72 Json(Vec<u8>),
73 Jsonl(Vec<u8>),
74 Yaml(Vec<u8>),
75 Text { bytes: Vec<u8>, mode: TextMode },
76 Fileset(Vec<FilesetInput>),
77}
78
79pub fn headson(
80 input: InputKind,
81 config: &RenderConfig,
82 priority_cfg: &PriorityConfig,
83 grep: &GrepConfig,
84 budgets: Budgets,
85) -> Result<RenderOutput> {
86 let crate::ingest::IngestOutput {
87 arena,
88 mut warnings,
89 } = crate::ingest::ingest_into_arena(input, priority_cfg, grep)?;
90 let mut order_build = order::build_order(&arena, priority_cfg)?;
91 if order_build.safety_cap_hit {
92 warnings.push(format!(
93 "warning: input truncated (exceeded {} node safety cap)",
94 priority_cfg.safety_cap
95 ));
96 }
97 let (text, match_summary) = find_largest_render_under_budgets(
98 &mut order_build,
99 config,
100 grep,
101 budgets,
102 );
103 Ok(RenderOutput {
104 text,
105 warnings,
106 match_summary,
107 })
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn test_render_config() -> RenderConfig {
115 RenderConfig {
116 template: OutputTemplate::Pseudo,
117 indent_unit: " ".to_string(),
118 space: " ".to_string(),
119 newline: "\n".to_string(),
120 color_mode: ColorMode::Off,
121 color_enabled: false,
122 style: serialization::types::Style::Default,
123 prefer_tail_arrays: false,
124 string_free_prefix_graphemes: None,
125 debug: false,
126 primary_source_name: None,
127 show_fileset_headers: false,
128 fileset_tree: false,
129 count_fileset_headers_in_budgets: false,
130 grep_highlight: None,
131 }
132 }
133
134 #[test]
135 fn safety_cap_warning_emitted_when_exceeded() {
136 let mut priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
140 priority_cfg.safety_cap = 5;
141
142 let result = headson(
143 InputKind::Json(b"[1,2,3,4,5]".to_vec()),
144 &test_render_config(),
145 &priority_cfg,
146 &GrepConfig::default(),
147 Budgets::default(),
148 )
149 .expect("headson should succeed");
150
151 assert!(
152 result.warnings.iter().any(|w| w.contains("safety cap")),
153 "expected safety cap warning, got: {:?}",
154 result.warnings
155 );
156 }
157
158 #[test]
159 fn no_safety_cap_warning_when_not_exceeded() {
160 let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
162
163 let result = headson(
164 InputKind::Json(b"[1,2,3]".to_vec()),
165 &test_render_config(),
166 &priority_cfg,
167 &GrepConfig::default(),
168 Budgets::default(),
169 )
170 .expect("headson should succeed");
171
172 assert!(
173 !result.warnings.iter().any(|w| w.contains("safety cap")),
174 "unexpected safety cap warning: {:?}",
175 result.warnings
176 );
177 }
178
179 #[test]
180 fn strong_grep_match_summary_hidden_zero_under_tight_budget() {
181 let input = br#"{"alpha": "needle one", "beta": "no match here", "gamma": "needle two"}"#;
185 let grep_cfg = build_grep_config(
186 Some("needle"),
187 None,
188 GrepShow::Matching,
189 false,
190 true,
191 )
192 .expect("valid grep pattern");
193 let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
194 let budgets = Budgets {
197 global: Some(Budget {
198 kind: BudgetKind::Lines,
199 cap: 1,
200 }),
201 per_slot: None,
202 };
203
204 let result = headson(
205 InputKind::Json(input.to_vec()),
206 &test_render_config(),
207 &priority_cfg,
208 &grep_cfg,
209 budgets,
210 )
211 .expect("headson should succeed");
212
213 let summary = result
214 .match_summary
215 .expect("match_summary must be Some when grep is active");
216 assert_eq!(
217 summary.hidden, 0,
218 "strong grep must force all matches into output; hidden should be 0, got {:?}",
219 result.match_summary
220 );
221 assert_eq!(
222 summary.shown, 2,
223 "exactly 2 values match 'needle'; shown should be 2, got {:?}",
224 result.match_summary
225 );
226 }
227
228 #[test]
229 fn strong_grep_match_summary_zero_matches() {
230 let input = br#"{"alpha": "apple", "beta": "banana"}"#;
231 let grep_cfg = build_grep_config(
232 Some("zzznomatch"),
233 None,
234 GrepShow::Matching,
235 false,
236 true,
237 )
238 .expect("valid grep pattern");
239 let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
240
241 let result = headson(
242 InputKind::Json(input.to_vec()),
243 &test_render_config(),
244 &priority_cfg,
245 &grep_cfg,
246 Budgets::default(),
247 )
248 .expect("headson should succeed");
249
250 let summary = result
251 .match_summary
252 .expect("match_summary must be Some when grep is active");
253 assert_eq!(
254 summary.shown, 0,
255 "pattern matches nothing; shown should be 0, got {:?}",
256 result.match_summary
257 );
258 assert_eq!(
259 summary.hidden, 0,
260 "pattern matches nothing; hidden should be 0, got {:?}",
261 result.match_summary
262 );
263 }
264
265 #[test]
266 fn weak_grep_match_summary_has_hidden_under_tight_budget() {
267 let input = br#"{
271 "a": "target one",
272 "b": "target two",
273 "c": "target three",
274 "d": "target four",
275 "e": "target five"
276 }"#;
277 let total_matches: usize = 5;
278 let grep_cfg = build_grep_config(
279 None,
280 Some("target"),
281 GrepShow::Matching,
282 false,
283 false,
284 )
285 .expect("valid weak grep pattern");
286 let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
287 let budgets = Budgets {
290 global: Some(Budget {
291 kind: BudgetKind::Lines,
292 cap: 4,
293 }),
294 per_slot: None,
295 };
296
297 let result = headson(
298 InputKind::Json(input.to_vec()),
299 &test_render_config(),
300 &priority_cfg,
301 &grep_cfg,
302 budgets,
303 )
304 .expect("headson should succeed");
305
306 let summary = result
307 .match_summary
308 .expect("match_summary must be Some when grep is active");
309 assert_eq!(
310 summary.shown + summary.hidden,
311 total_matches,
312 "shown + hidden must equal total direct matches ({}); got shown={} hidden={}",
313 total_matches,
314 summary.shown,
315 summary.hidden,
316 );
317 assert!(
318 summary.hidden > 0,
319 "tight budget must cause some weak-grep matches to be hidden; \
320 got shown={} hidden={}",
321 summary.shown,
322 summary.hidden,
323 );
324 }
325
326 #[test]
327 fn weak_grep_match_summary_all_shown_under_loose_budget() {
328 let input = br#"{
331 "a": "target one",
332 "b": "target two",
333 "c": "target three",
334 "d": "target four",
335 "e": "target five"
336 }"#;
337 let total_matches: usize = 5;
338 let grep_cfg = build_grep_config(
339 None,
340 Some("target"),
341 GrepShow::Matching,
342 false,
343 false,
344 )
345 .expect("valid weak grep pattern");
346 let priority_cfg = PriorityConfig::new(usize::MAX, usize::MAX);
347
348 let result = headson(
349 InputKind::Json(input.to_vec()),
350 &test_render_config(),
351 &priority_cfg,
352 &grep_cfg,
353 Budgets::default(),
354 )
355 .expect("headson should succeed");
356
357 let summary = result
358 .match_summary
359 .expect("match_summary must be Some when grep is active");
360 assert_eq!(
361 summary.shown, total_matches,
362 "loose budget must show all {} matches; got shown={} hidden={}",
363 total_matches, summary.shown, summary.hidden,
364 );
365 assert_eq!(
366 summary.hidden, 0,
367 "no matches should be hidden under a loose budget; \
368 got shown={} hidden={}",
369 summary.shown, summary.hidden,
370 );
371 }
372}