mahbot 0.1.1

An autonomous agentic engineering system that manages software development through role separation, subagents, and deterministic diagnostics.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
//! Shared dashboard widgets: styled pick_list, PickOption type, FileTree state struct
//! and build_tree_panel for shared file-tree panel rendering.

use std::collections::HashSet;
use std::time::Duration;

use iced::widget::{self, button, column, container, pick_list, text, text_input};
use iced::{Alignment, Color, Element, Length, Padding, Task};

use iced_selection;

use super::theme;

/// An option for [`fn@pick_list`] with separate value and display label.
///
/// Equality is determined by `value` only — two `PickOption`s with the same
/// `value` are considered equal regardless of label. This lets [`fn@pick_list`]
/// highlight the correct option even when the selected value is constructed
/// independently of the options list.
#[derive(Debug, Clone)]
pub struct PickOption {
    pub value: String,
    pub label: String,
}

impl PartialEq for PickOption {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

impl Eq for PickOption {}

impl std::fmt::Display for PickOption {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.label)
    }
}

/// Flexoki-dark themed style for [`fn@pick_list`] widgets.
pub fn pick_list_style(_theme: &iced::Theme, _status: pick_list::Status) -> pick_list::Style {
    pick_list::Style {
        text_color: theme::TEXT_PRIMARY,
        placeholder_color: theme::TEXT_MUTED,
        handle_color: theme::TEXT_MUTED,
        background: iced::Background::Color(theme::BG_ELEVATED),
        border: iced::Border {
            radius: 4.0.into(),
            width: 1.0,
            color: theme::BORDER_STRONG,
        },
    }
}

/// Flexoki-dark themed style for [`fn@text_input`] widgets.
/// Matches [`pick_list_style`] for visual consistency.
pub fn text_input_style(_theme: &iced::Theme, _status: text_input::Status) -> text_input::Style {
    text_input::Style {
        background: iced::Background::Color(theme::BG_ELEVATED),
        border: iced::Border {
            radius: 4.0.into(),
            width: 1.0,
            color: theme::BORDER_STRONG,
        },
        icon: theme::TEXT_MUTED,
        placeholder: theme::TEXT_MUTED,
        value: theme::TEXT_PRIMARY,
        selection: theme::ACCENT,
    }
}

/// Render a styled error banner for dashboard panels.
pub fn error_banner<'a, Message: 'a>(err: &'a str) -> Element<'a, Message> {
    container(text(err).size(13).color(theme::STATUS_ERROR))
        .padding(8)
        .style(|_theme: &iced::Theme| container::Style {
            background: Some(iced::Background::Color(iced::Color::from_rgba(
                1.0, 0.267, 0.4, 0.08,
            ))),
            border: iced::Border {
                radius: 4.0.into(),
                ..iced::Border::default()
            },
            ..container::Style::default()
        })
        .into()
}

/// Render a centered empty-state placeholder with a lucide icon and label.
pub fn empty_state_placeholder<'a, Message: 'a>(
    icon: iced::widget::Text<'a, iced::Theme, iced::Renderer>,
    label: &'a str,
) -> Element<'a, Message> {
    container(
        column![
            icon.size(48).color(theme::TEXT_MUTED),
            text(label).size(14).color(theme::TEXT_MUTED),
        ]
        .spacing(12)
        .align_x(Alignment::Center),
    )
    .width(Length::Fill)
    .height(Length::Fill)
    .center_x(Length::Fill)
    .center_y(Length::Fill)
    .into()
}

/// Create a selectable text widget with the given color.
///
/// Accepts both borrowed (`&str`) and owned (`String`) text content.
pub fn selectable_text<'a>(
    content: impl iced_selection::text::IntoFragment<'a>,
    color: Color,
) -> iced_selection::text::Text<'a, iced::Theme, iced::Renderer> {
    iced_selection::text::Text::new(content).style(move |_theme| iced_selection::text::Style {
        color: Some(color),
        ..Default::default()
    })
}

// ── Debounce helpers ───────────────────────────────────────────────

/// Spawn a sleep task that returns `generation` after `ms` milliseconds.
///
/// Use with a debounced refresh message to avoid multiple rapid refreshes:
/// increment a generation counter, spawn this task with the new generation,
/// and in the response handler check [`debounce_should_process`].
pub async fn debounce_sleep(ms: u64, generation: u64) -> u64 {
    tokio::time::sleep(Duration::from_millis(ms)).await;
    generation
}

/// Returns `true` if a debounced refresh should proceed.
///
/// Pass the generation from the debounce response, the current generation
/// counter, and the pending flag.  This prevents stale debounce tasks
/// from triggering a refresh after a newer task has been spawned.
#[must_use]
pub const fn debounce_should_process(
    generation: u64,
    current_generation: u64,
    pending: bool,
) -> bool {
    generation == current_generation && pending
}

// ── File tree ───────────────────────────────────────────────────────

/// A node in a shared file-tree sidebar.
#[derive(Debug, Clone)]
pub struct TreeNode {
    /// Display name (directory or file name component).
    pub name: String,
    /// Full relative path from workspace/repo root.
    pub full_path: String,
    /// Whether this is a directory node.
    pub is_dir: bool,
    /// Children (only populated for expanded directory nodes).
    pub children: Vec<TreeNode>,
    /// Error message if this entry couldn't be inspected (broken symlink, etc.).
    pub error: Option<String>,
}

/// Shared file-tree state used by both the editor and diff dashboard pages.
pub struct FileTree {
    /// The hierarchical tree nodes.
    pub nodes: Vec<TreeNode>,
    /// Which directories are expanded (by `full_path`).
    pub expanded_dirs: HashSet<String>,
    /// Whether keyboard focus is in the file tree.
    pub tree_focused: bool,
    /// Index into `visible_tree_nodes` of the focused entry.
    pub tree_focus_index: usize,
    /// Flattened visible tree entries: (full_path, is_dir).
    pub visible_tree_nodes: Vec<(String, bool)>,
    /// Scrollable ID for the tree panel (for scroll-into-view).
    pub tree_scroll_id: iced::widget::Id,
}

impl FileTree {
    /// Create a new empty `FileTree` with the given scrollable ID.
    #[must_use]
    pub fn new(scroll_id: iced::widget::Id) -> Self {
        Self {
            nodes: Vec::new(),
            expanded_dirs: HashSet::new(),
            tree_focused: false,
            tree_focus_index: 0,
            visible_tree_nodes: Vec::new(),
            tree_scroll_id: scroll_id,
        }
    }

    /// Rebuild the flattened list of visible tree nodes for keyboard navigation.
    pub fn rebuild_visible(&mut self) {
        self.visible_tree_nodes.clear();
        Self::flatten_tree_nodes(
            &self.nodes,
            &self.expanded_dirs,
            &mut self.visible_tree_nodes,
        );
        if self.visible_tree_nodes.is_empty() {
            self.tree_focus_index = 0;
        } else {
            self.tree_focus_index = self.tree_focus_index.min(self.visible_tree_nodes.len() - 1);
        }
    }

    /// Recursively flatten tree nodes, respecting expanded state.
    fn flatten_tree_nodes(
        nodes: &[TreeNode],
        expanded: &HashSet<String>,
        out: &mut Vec<(String, bool)>,
    ) {
        for node in nodes {
            out.push((node.full_path.clone(), node.is_dir));
            if node.is_dir && expanded.contains(&node.full_path) && !node.children.is_empty() {
                Self::flatten_tree_nodes(&node.children, expanded, out);
            }
        }
    }

    /// Sort tree nodes: directories first, then case-insensitive alphabetical.
    /// Applied recursively so subdirectory children are also sorted.
    pub fn sort_nodes(nodes: &mut [TreeNode]) {
        nodes.sort_by(|a, b| {
            if a.is_dir != b.is_dir {
                return b.is_dir.cmp(&a.is_dir);
            }
            a.name.to_lowercase().cmp(&b.name.to_lowercase())
        });
        for node in nodes {
            Self::sort_nodes(&mut node.children);
        }
    }

    /// Clear all tree state (nodes, expanded dirs, visible list, focus).
    pub fn clear(&mut self) {
        self.nodes.clear();
        self.expanded_dirs.clear();
        self.tree_focused = false;
        self.tree_focus_index = 0;
        self.visible_tree_nodes.clear();
    }

    /// Set the focus index to the visible-tree position of `path`, if found.
    ///
    /// Returns the found position, or [`None`] if `path` is not in the visible tree.
    /// The caller can use the returned position for additional logic (e.g. advancing
    /// focus past a directory to its first child).
    pub fn focus_path(&mut self, path: &str) -> Option<usize> {
        let pos = self
            .visible_tree_nodes
            .iter()
            .position(|(p, _)| p == path)?;
        self.tree_focus_index = pos;
        Some(pos)
    }
}

/// Font size for file tree item labels and connector guides.
pub const TREE_FONT_SIZE: f32 = 14.0;

/// Icon size for directory nodes in the file tree (slightly larger than
/// [`TREE_FONT_SIZE`] to compensate for lucide icons appearing smaller
/// at the same nominal point size).
pub const TREE_ICON_SIZE: f32 = 15.0;

/// Estimated height per tree row for scroll-into-view on keyboard navigation.
pub const ESTIMATED_TREE_ROW_HEIGHT: f32 = 20.0;

/// Scroll the tree panel to bring the focused row into view.
#[allow(clippy::cast_precision_loss)]
pub fn scroll_to_tree_focus<Message: 'static>(file_tree: &FileTree) -> Task<Message> {
    if file_tree.visible_tree_nodes.is_empty() {
        return Task::none();
    }
    let offset_y = file_tree.tree_focus_index as f32 * ESTIMATED_TREE_ROW_HEIGHT;
    iced::widget::operation::scroll_to(
        file_tree.tree_scroll_id.clone(),
        iced::widget::operation::AbsoluteOffset {
            x: 0.0,
            y: offset_y,
        },
    )
}

/// Build a file-tree panel widget.
///
/// Renders a scrollable, fixed-width column wrapping the pre-built
/// `tree_element` rows. A focus border is applied when `file_tree.tree_focused`
/// is true.
pub fn build_tree_panel<'a, Message: 'a>(
    file_tree: &'a FileTree,
    tree_rows: Vec<Element<'a, Message>>,
) -> Element<'a, Message> {
    let tree_body = widget::scrollable(column(tree_rows).spacing(0))
        .id(file_tree.tree_scroll_id.clone())
        .width(Length::Fill)
        .height(Length::Fill)
        .direction(widget::scrollable::Direction::Vertical(
            theme::thin_scrollbar(),
        ))
        .style(theme::scrollbar_style);

    let tree_inner: Element<'_, Message> = container(tree_body)
        .width(Length::Fixed(260.0))
        .height(Length::Fill)
        .style(|_t: &iced::Theme| container::Style {
            background: Some(iced::Background::Color(theme::BG_SURFACE)),
            border: iced::Border {
                radius: 0.0.into(),
                width: 0.0,
                color: iced::Color::TRANSPARENT,
            },
            ..Default::default()
        })
        .into();

    if file_tree.tree_focused {
        container(tree_inner)
            .style(|_t: &iced::Theme| container::Style {
                border: iced::Border {
                    color: theme::ACCENT_LIGHT,
                    width: 2.0,
                    radius: 0.0.into(),
                },
                ..Default::default()
            })
            .into()
    } else {
        tree_inner
    }
}

// ── Tree node helpers ──────────────────────────────────────────────

/// Build the guide-line prefix string for a tree node.
///
/// Returns box-drawing characters that visually connect tree siblings:
///
/// | Character | Meaning |
/// |---|---|
/// | `│` | Vertical continuation — the ancestor at this depth has more siblings below |
/// | `├` | T-junction — this node has at least one more sibling after it |
/// | `└` | Corner — this node is the last child of its parent |
/// | ` `  | No continuation at this ancestor level |
///
/// Each depth level uses exactly two characters (guide char + one space), so
/// the total visual width per level closely matches the existing 14 px indent.
///
/// `ancestor_mask` has bit `d` set iff the ancestor at depth `d` has more
/// siblings after it (requiring a vertical continuation line at that column).
/// `depth` is the current nesting depth (0 = root, which gets no prefix).
/// `is_last` is true when this node is the last child of its parent.
///
/// # Panics
///
/// Panics in debug builds when `depth >= 64` (the u64 bitmask would overflow).
#[must_use]
pub fn tree_guide_prefix(ancestor_mask: u64, depth: usize, is_last: bool) -> String {
    debug_assert!(
        depth < 64,
        "tree_guide_prefix: depth {depth} exceeds u64 bit limit (max 63)"
    );
    let mut s = String::new();
    for d in 0..depth.saturating_sub(1) {
        if ancestor_mask & (1u64 << d) != 0 {
            s.push('');
        } else {
            s.push(' ');
        }
        s.push(' ');
    }
    if depth > 0 {
        if is_last {
            s.push('');
        } else {
            s.push('');
        }
        s.push(' ');
    }
    s
}

/// Recursively render children of a tree node, computing the correct
/// continuation mask and `is_last` state for each child.
///
/// `render_node` is called for each child with `(child, depth+1, child_mask, child_is_last)`.
/// The returned elements share the lifetime `'a` of the tree nodes.
/// Returns a `Vec` of child elements, one per child, in order.
///
/// This exists to avoid duplicating the child-iteration + mask-computation
/// logic across the two render paths (editor and diff file trees).
pub fn render_tree_children<'a, Message>(
    children: &'a [TreeNode],
    depth: usize,
    ancestor_mask: u64,
    is_last: bool,
    render_node: impl Fn(&'a TreeNode, usize, u64, bool) -> Element<'a, Message>,
) -> Vec<Element<'a, Message>> {
    let child_count = children.len();
    let cont_bit = if !is_last { 1u64 << depth } else { 0u64 };
    let child_mask = ancestor_mask | cont_bit;
    children
        .iter()
        .enumerate()
        .map(|(i, child)| {
            let child_is_last = i == child_count - 1;
            render_node(child, depth + 1, child_mask, child_is_last)
        })
        .collect()
}

/// Check whether a tree node at the given path is currently focused
/// in the file tree's keyboard navigation.
#[must_use]
pub fn tree_node_focused(tree: &FileTree, node_path: &str) -> bool {
    tree.tree_focused
        && tree.tree_focus_index < tree.visible_tree_nodes.len()
        && tree.visible_tree_nodes[tree.tree_focus_index].0 == node_path
}

/// Return a button style closure for tree node entries.
/// When `is_highlighted` is true, uses [`theme::HOVER_STRONG`]; otherwise
/// hover gets [`theme::HOVER`], and default is transparent.
fn tree_node_button_style(
    is_highlighted: bool,
) -> impl Fn(&iced::Theme, button::Status) -> button::Style {
    move |_t: &iced::Theme, status| {
        let bg = if is_highlighted {
            theme::HOVER_STRONG
        } else if status == button::Status::Hovered {
            theme::HOVER
        } else {
            iced::Color::TRANSPARENT
        };
        button::Style {
            background: Some(iced::Background::Color(bg)),
            ..Default::default()
        }
    }
}

/// Build a tree-node button from a content row, highlight state, and
/// optional press message. Uses `tree_node_button_style` internally
/// and spans full width.
///
/// This returns only the button element — callers that need context menus
/// (e.g., the editor page) must wrap the result themselves.
pub fn tree_node_button<'a, Message: Clone + 'a>(
    content: impl Into<Element<'a, Message>>,
    is_highlighted: bool,
    on_press: Option<Message>,
) -> Element<'a, Message> {
    let mut btn = widget::button(content)
        .style(tree_node_button_style(is_highlighted))
        .width(Length::Fill)
        .padding(Padding::ZERO);
    if let Some(msg) = on_press {
        btn = btn.on_press(msg);
    }
    btn.into()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Helper to create a FileTree with known visible_tree_nodes for testing.
    fn make_tree(nodes: Vec<(&str, bool)>) -> FileTree {
        let mut tree = FileTree::new(iced::widget::Id::new("test"));
        tree.visible_tree_nodes = nodes
            .into_iter()
            .map(|(p, is_dir)| (p.to_string(), is_dir))
            .collect();
        tree
    }

    #[test]
    fn focus_path_found() {
        let mut tree = make_tree(vec![
            ("src", true),
            ("src/main.rs", false),
            ("Cargo.toml", false),
        ]);
        assert_eq!(tree.focus_path("src/main.rs"), Some(1));
        assert_eq!(tree.tree_focus_index, 1);
    }

    #[test]
    fn focus_path_not_found() {
        let mut tree = make_tree(vec![("src", true), ("Cargo.toml", false)]);
        tree.tree_focus_index = 42;
        assert_eq!(tree.focus_path("nonexistent"), None);
        assert_eq!(tree.tree_focus_index, 42);
    }

    #[test]
    fn focus_path_empty_tree() {
        let mut tree = make_tree(vec![]);
        assert_eq!(tree.focus_path("anything"), None);
        assert_eq!(tree.tree_focus_index, 0);
    }

    #[test]
    fn focus_path_first_node() {
        let mut tree = make_tree(vec![("src", true), ("src/main.rs", false)]);
        assert_eq!(tree.focus_path("src"), Some(0));
        assert_eq!(tree.tree_focus_index, 0);
    }

    #[test]
    fn focus_path_updates_index_no_residual() {
        let mut tree = make_tree(vec![("a", false), ("b", false), ("c", false)]);
        // Focus on "c", then re-focus on "a" — should end up at index 0.
        tree.focus_path("c");
        assert_eq!(tree.tree_focus_index, 2);
        tree.focus_path("a");
        assert_eq!(tree.tree_focus_index, 0);
    }

    // ── tree_guide_prefix tests ─────────────────────────────────────

    /// Build the expected guide string from a literal. Makes it easier to
    /// see the box-drawing characters in test output.
    fn g(guide: &str) -> String {
        guide.to_string()
    }

    #[test]
    fn guide_prefix_depth_zero() {
        // Root-level nodes have no guide lines regardless of mask or is_last.
        assert_eq!(tree_guide_prefix(0, 0, false), g(""));
        assert_eq!(tree_guide_prefix(0, 0, true), g(""));
        assert_eq!(tree_guide_prefix(0b_1111, 0, false), g(""));
    }

    #[test]
    fn guide_prefix_depth_one_not_last() {
        // Depth 1, no ancestor continuation, non-last child.
        // Just the connector (no ancestor guide needed — connector replaces
        // the single ancestor slot).
        assert_eq!(tree_guide_prefix(0, 1, false), g(""));
    }

    #[test]
    fn guide_prefix_depth_one_last() {
        // Depth 1, no ancestor continuation, last child → └ connector.
        assert_eq!(tree_guide_prefix(0, 1, true), g(""));
    }

    #[test]
    fn guide_prefix_depth_one_ancestor_continues_not_last() {
        // Depth 1, ancestor at depth 0 has continuation (bit 0 = 1).
        // Connector replaces the depth-0 ancestor slot, showing just ├.
        assert_eq!(tree_guide_prefix(0b_01, 1, false), g(""));
    }

    #[test]
    fn guide_prefix_depth_one_ancestor_continues_last() {
        // Depth 1, ancestor continues, current node last → └
        assert_eq!(tree_guide_prefix(0b_01, 1, true), g(""));
    }

    #[test]
    fn guide_prefix_depth_two_mixed_ancestors() {
        // Depth 2: ancestor depth 0 continues, depth 1 does NOT continue.
        // mask = bit 0 set, bit 1 clear.
        // Only the depth-0 ancestor appears as a guide; depth-1 slot is
        // replaced by the connector.
        assert_eq!(tree_guide_prefix(0b_01, 2, false), g("│ ├ "));
    }

    #[test]
    fn guide_prefix_depth_two_all_ancestors_continue_not_last() {
        // Both ancestors continue (mask bits 0 and 1 set).
        // Only depth-0 guide appears; depth-1 replaced by connector.
        assert_eq!(tree_guide_prefix(0b_11, 2, false), g("│ ├ "));
    }

    #[test]
    fn guide_prefix_depth_two_all_ancestors_continue_last() {
        // Both ancestors continue, current node last → "│ └ "
        assert_eq!(tree_guide_prefix(0b_11, 2, true), g("│ └ "));
    }

    #[test]
    fn guide_prefix_depth_two_no_ancestor_continuation() {
        // Depth 2: neither ancestor continues (mask bits 0 and 1 clear).
        // Current node not last → two spaces for depth 0 + ├ connector.
        assert_eq!(tree_guide_prefix(0, 2, false), g(""));
    }

    #[test]
    fn guide_prefix_depth_two_no_ancestor_continuation_last() {
        // Depth 2: no ancestor continuation, last child → "  └ "
        assert_eq!(tree_guide_prefix(0, 2, true), g(""));
    }

    #[test]
    fn guide_prefix_deep_tree() {
        // Depth 5, ancestors at 0,1,3 continue; 2 does not; 4 is the slot
        // replaced by the connector.
        // mask bits set: 0, 1, 3  →  binary 0b_1011
        // Only 4 ancestor guides (depths 0-3), connector replaces depth 4.
        //            d0 d1 d2 d3
        //            │  │  sp │
        assert_eq!(tree_guide_prefix(0b_1011, 5, false), g("│ │   │ ├ "));
    }

    #[test]
    fn guide_prefix_deep_tree_last() {
        // Same as above but last child → └ instead of ├
        assert_eq!(tree_guide_prefix(0b_1011, 5, true), g("│ │   │ └ "));
    }

    #[test]
    fn guide_prefix_mask_ignores_bits_above_depth() {
        // Bits beyond depth should be ignored. depth=1 with high bits set.
        assert_eq!(tree_guide_prefix(0b1_0000_0000, 1, false), g(""));
        assert_eq!(tree_guide_prefix(0b1_0000_0000, 1, true), g(""));
    }

    #[test]
    #[should_panic(expected = "exceeds u64 bit limit")]
    fn guide_prefix_depth_overflow_debug() {
        // debug_assert fires at depth >= 64 in debug builds.
        let _ = tree_guide_prefix(0, 64, false);
    }
}