1use crate::types::SlopNode;
5
6#[derive(Debug, Clone, Default)]
8pub struct OutputTreeOptions {
9 pub max_depth: Option<usize>,
11 pub max_nodes: Option<usize>,
13 pub min_salience: Option<f64>,
15 pub types: Option<Vec<String>>,
17}
18
19pub fn prepare_tree(root: &SlopNode, options: &OutputTreeOptions) -> SlopNode {
21 let mut tree = root.clone();
22 if options.min_salience.is_some() || options.types.is_some() {
23 tree = filter_tree(&tree, options.min_salience, options.types.as_deref());
24 }
25 if let Some(max_depth) = options.max_depth {
26 tree = truncate_tree(&tree, max_depth as i32);
27 }
28 if let Some(max_nodes) = options.max_nodes {
29 tree = auto_compact(&tree, max_nodes);
30 }
31 tree
32}
33
34pub fn get_subtree<'a>(root: &'a SlopNode, path: &str) -> Option<&'a SlopNode> {
36 if path.is_empty() || path == "/" {
37 return Some(root);
38 }
39 let segments: Vec<&str> = path
40 .trim_start_matches('/')
41 .split('/')
42 .filter(|s| !s.is_empty())
43 .collect();
44 let mut current = root;
45 for seg in segments {
46 let children = current.children.as_ref()?;
47 current = children.iter().find(|c| c.id == seg)?;
48 }
49 Some(current)
50}
51
52pub fn truncate_tree(node: &SlopNode, depth: i32) -> SlopNode {
59 if depth <= 0 {
60 if let Some(children) = &node.children {
61 if !children.is_empty() {
62 let mut meta = node.meta.clone().unwrap_or_default();
63 meta.total_children = Some(children.len());
64 return SlopNode {
65 id: node.id.clone(),
66 node_type: node.node_type.clone(),
67 properties: None,
68 children: None,
69 affordances: None,
70 meta: Some(meta),
71 content_ref: None,
72 };
73 }
74 }
75 }
76 match &node.children {
77 None => node.clone(),
78 Some(children) => {
79 let mut out = node.clone();
80 out.children = Some(
81 children
82 .iter()
83 .map(|c| truncate_tree(c, depth - 1))
84 .collect(),
85 );
86 out
87 }
88 }
89}
90
91pub fn auto_compact(root: &SlopNode, max_nodes: usize) -> SlopNode {
94 let total = count_nodes(root);
95 if total <= max_nodes {
96 return root.clone();
97 }
98
99 let mut candidates = Vec::new();
100 if let Some(children) = &root.children {
101 for (i, child) in children.iter().enumerate() {
102 collect_candidates(child, &[i], &mut candidates, false);
103 }
104 }
105
106 candidates.sort_by(|a, b| {
107 a.score
108 .partial_cmp(&b.score)
109 .unwrap_or(std::cmp::Ordering::Equal)
110 });
111
112 let mut tree = root.clone();
113 let mut node_count = total;
114
115 for candidate in &candidates {
116 if node_count <= max_nodes {
117 break;
118 }
119 let saved = collapse_at_path(&mut tree, &candidate.path);
120 node_count -= saved;
121 }
122
123 tree
124}
125
126pub fn filter_tree(
129 node: &SlopNode,
130 min_salience: Option<f64>,
131 types: Option<&[String]>,
132) -> SlopNode {
133 let children = match &node.children {
134 None => return node.clone(),
135 Some(c) => c,
136 };
137
138 let filtered: Vec<SlopNode> = children
139 .iter()
140 .filter(|child| {
141 if let Some(ms) = min_salience {
142 let s = child.meta.as_ref().and_then(|m| m.salience).unwrap_or(0.5);
143 if s < ms {
144 return false;
145 }
146 }
147 if let Some(t) = types {
148 if !t.iter().any(|ty| ty == &child.node_type) {
149 return false;
150 }
151 }
152 true
153 })
154 .map(|child| filter_tree(child, min_salience, types))
155 .collect();
156
157 let mut out = node.clone();
158 out.children = if filtered.is_empty() {
159 None
160 } else {
161 Some(filtered)
162 };
163 out
164}
165
166pub fn count_nodes(node: &SlopNode) -> usize {
168 1 + node
169 .children
170 .as_ref()
171 .map(|c| c.iter().map(count_nodes).sum())
172 .unwrap_or(0)
173}
174
175struct CompactCandidate {
178 path: Vec<usize>,
179 score: f64,
180 #[allow(dead_code)]
181 child_count: usize,
182}
183
184fn collect_candidates(
185 node: &SlopNode,
186 path: &[usize],
187 candidates: &mut Vec<CompactCandidate>,
188 is_root_child: bool,
189) {
190 let children = match &node.children {
191 None => return,
192 Some(c) => c,
193 };
194 for (i, child) in children.iter().enumerate() {
195 let mut child_path = path.to_vec();
196 child_path.push(i);
197
198 let pinned = child.meta.as_ref().and_then(|m| m.pinned).unwrap_or(false);
199 let has_children = child.children.as_ref().is_some_and(|c| !c.is_empty());
200
201 if has_children && !is_root_child && !pinned {
202 let child_count = count_nodes(child) - 1;
203 let salience = child.meta.as_ref().and_then(|m| m.salience).unwrap_or(0.5);
204 let depth = child_path.len() as f64;
205 let score = salience - depth * 0.01 - child_count as f64 * 0.001;
206 candidates.push(CompactCandidate {
207 path: child_path.clone(),
208 score,
209 child_count,
210 });
211 }
212
213 collect_candidates(child, &child_path, candidates, false);
214 }
215}
216
217fn collapse_at_path(tree: &mut SlopNode, path: &[usize]) -> usize {
218 let mut node = tree;
219 for &idx in &path[..path.len() - 1] {
220 let children = match &mut node.children {
221 Some(c) if idx < c.len() => c,
222 _ => return 0,
223 };
224 node = &mut children[idx];
225 }
226
227 let last_idx = path[path.len() - 1];
228 let children = match &mut node.children {
229 Some(c) if last_idx < c.len() => c,
230 _ => return 0,
231 };
232
233 let target = &children[last_idx];
234 let saved = count_nodes(target) - 1;
235 let tc = target.children.as_ref().map_or(0, |c| c.len());
236
237 let mut meta = target.meta.clone().unwrap_or_default();
238 meta.total_children = Some(tc);
239 if meta.summary.is_none() {
240 meta.summary = Some(format!("{} children", tc));
241 }
242
243 children[last_idx] = SlopNode {
244 id: target.id.clone(),
245 node_type: target.node_type.clone(),
246 properties: target.properties.clone(),
247 children: None,
248 affordances: target.affordances.clone(),
249 meta: Some(meta),
250 content_ref: target.content_ref.clone(),
251 };
252
253 saved
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::types::NodeMeta;
260
261 fn make_node(id: &str, node_type: &str) -> SlopNode {
262 SlopNode::new(id, node_type)
263 }
264
265 fn make_tree() -> SlopNode {
266 let mut root = make_node("root", "root");
267 let mut inbox = make_node("inbox", "view");
268 inbox.meta = Some(NodeMeta {
269 salience: Some(0.8),
270 ..Default::default()
271 });
272 let msg1 = make_node("msg-1", "item");
273 let msg2 = make_node("msg-2", "item");
274 inbox.children = Some(vec![msg1, msg2]);
275
276 let mut settings = make_node("settings", "view");
277 settings.meta = Some(NodeMeta {
278 salience: Some(0.1),
279 ..Default::default()
280 });
281 let mut general = make_node("general", "group");
282 general.children = Some(vec![make_node("theme", "item")]);
283 settings.children = Some(vec![general]);
284
285 root.children = Some(vec![inbox, settings]);
286 root
287 }
288
289 #[test]
290 fn test_count_nodes() {
291 let tree = make_tree();
292 assert_eq!(count_nodes(&tree), 7);
293 }
294
295 #[test]
296 fn test_get_subtree() {
297 let tree = make_tree();
298 let sub = get_subtree(&tree, "/inbox").unwrap();
299 assert_eq!(sub.id, "inbox");
300 let msg = get_subtree(&tree, "/inbox/msg-1").unwrap();
301 assert_eq!(msg.id, "msg-1");
302 assert!(get_subtree(&tree, "/nonexistent").is_none());
303 }
304
305 #[test]
306 fn test_truncate_tree() {
307 let tree = make_tree();
308 let truncated = truncate_tree(&tree, 1);
309 assert!(truncated.children.is_some());
310 let children = truncated.children.unwrap();
311 assert!(children[0].children.is_none());
313 assert_eq!(children[0].meta.as_ref().unwrap().total_children, Some(2));
314 }
315
316 #[test]
317 fn test_filter_tree() {
318 let tree = make_tree();
319 let filtered = filter_tree(&tree, Some(0.5), None);
320 let children = filtered.children.unwrap();
321 assert_eq!(children.len(), 1); assert_eq!(children[0].id, "inbox");
323 }
324
325 #[test]
326 fn test_auto_compact() {
327 let tree = make_tree();
328 let compacted = auto_compact(&tree, 6);
330 assert!(count_nodes(&compacted) <= 6);
331 let settings = get_subtree(&compacted, "/settings").unwrap();
333 let general = &settings.children.as_ref().unwrap()[0];
334 assert!(general.children.is_none());
335 assert!(general.meta.as_ref().unwrap().total_children.is_some());
336 }
337
338 #[test]
339 fn test_prepare_tree() {
340 let tree = make_tree();
341 let opts = OutputTreeOptions {
342 max_depth: Some(1),
343 min_salience: Some(0.5),
344 ..Default::default()
345 };
346 let prepared = prepare_tree(&tree, &opts);
347 let children = prepared.children.unwrap();
348 assert_eq!(children.len(), 1); assert!(children[0].children.is_none()); }
351}