Skip to main content

dbmd_core/
stats.rs

1//! `stats` — store overview, **computed on demand** (a SWEEP, like `du` —
2//! never a maintained or precomputed cache).
3//!
4//! Serves both the human (how big is my brain, what's the shape) and the agent
5//! (orientation). Deliberately excludes graph density / degree / top-linked
6//! analytics — low agent value, and a human who wants graph metrics opens the
7//! store in Obsidian, so we never build the full graph just for stats.
8
9use std::collections::{BTreeMap, HashSet};
10use std::path::{Path, PathBuf};
11
12use regex::Regex;
13
14use crate::store::{Layer, Store};
15
16/// A point-in-time overview of a store. Pure data; the CLI formats it to text
17/// or JSON.
18#[derive(Debug, Clone, Default, PartialEq)]
19pub struct Stats {
20    /// Total content-file count across all layers.
21    pub total_files: usize,
22    /// File count per layer.
23    pub files_per_layer: BTreeMap<Layer, usize>,
24    /// Total size on disk, in bytes.
25    pub total_size_bytes: u64,
26    /// Count per `type:` value (the type distribution).
27    pub type_distribution: BTreeMap<String, usize>,
28    /// Number of orphan files (no incoming and no outgoing wiki-links).
29    pub orphan_count: usize,
30    /// Number of broken wiki-links (target file doesn't exist).
31    pub broken_link_count: usize,
32    /// Top types by count, descending (ties broken by type name ascending).
33    pub top_types: Vec<(String, usize)>,
34}
35
36/// How many entries [`Stats::top_types`] holds.
37const TOP_TYPES_LIMIT: usize = 10;
38
39/// One content file discovered by the SWEEP, with everything `stats` needs:
40/// where it lives, how big it is, its declared `type`, and the wiki-link
41/// targets it emits (store-relative, `.md` stripped, short-form excluded).
42struct FileFacts {
43    /// Store-relative path *without* the `.md` extension — the node id used to
44    /// resolve wiki-links and detect orphans.
45    node_id: PathBuf,
46    /// The layer this file lives under.
47    layer: Layer,
48    /// File size on disk, in bytes.
49    size_bytes: u64,
50    /// The declared `type:`, if the frontmatter has one.
51    type_: Option<String>,
52    /// Every wiki-link target this file emits, store-relative with any trailing
53    /// `.md` stripped, in source order (not deduped, short-form included).
54    /// Resolved against the complete node set in a second pass.
55    raw_targets: Vec<PathBuf>,
56}
57
58impl FileFacts {
59    /// The subset of [`raw_targets`](FileFacts::raw_targets) that could resolve
60    /// to a store node: full store-relative paths. Short-form targets (no `/`)
61    /// are dropped — they're a `WIKI_LINK_SHORT_FORM` validation error, not a
62    /// graph edge, so stats neither counts them as broken nor lets them wire a
63    /// file out of orphan status.
64    fn resolvable_targets(&self) -> impl Iterator<Item = &PathBuf> {
65        self.raw_targets.iter().filter(|t| is_full_path(t))
66    }
67}
68
69/// **SWEEP.** Walk the store once and compute its [`Stats`]. Run occasionally
70/// (overview / orientation), never on the interactive loop.
71pub fn compute(store: &Store) -> crate::Result<Stats> {
72    let link_re = wiki_link_regex();
73
74    // First pass: walk every layer once, recording per-file facts and the set
75    // of node ids that exist on disk. Link resolution waits for the second
76    // pass, once every node's existence is known.
77    let mut existing_nodes: HashSet<PathBuf> = HashSet::new();
78    let mut facts: Vec<FileFacts> = Vec::new();
79
80    for layer in Layer::all() {
81        for rel in store.walk_layer(layer)? {
82            if rel
83                .components()
84                .nth(1)
85                .is_some_and(|component| component.as_os_str() == "log")
86            {
87                continue;
88            }
89            let node_id = strip_md(&rel);
90            existing_nodes.insert(node_id.clone());
91
92            let opened = store.open_regular(&rel).ok();
93            let size_bytes = opened
94                .as_ref()
95                .and_then(|file| file.metadata().ok())
96                .map(|metadata| metadata.len())
97                .unwrap_or(0);
98            let text = store
99                .read_text_bounded(&rel, crate::parser::MAX_DBMD_FILE_BYTES)
100                .unwrap_or_default();
101            let type_ = parse_type(&text);
102            let raw_targets = extract_link_targets(&text, &link_re);
103
104            facts.push(FileFacts {
105                node_id,
106                layer,
107                size_bytes,
108                type_,
109                raw_targets,
110            });
111        }
112    }
113
114    // Second pass: classify every file's links against the complete node set,
115    // counting broken links (full-path targets with no file on disk) and
116    // recording which nodes receive an incoming edge. Short-form targets are a
117    // validation error elsewhere, not a stats edge, so they're skipped here:
118    // they neither wire a file in nor count as broken.
119    let mut stats = Stats::default();
120    let mut linked_to: HashSet<PathBuf> = HashSet::new();
121    for file in &facts {
122        for target in file.resolvable_targets() {
123            // A self-link is not a graph edge — skip it (matches `graph::orphans`,
124            // so the two surfaces agree on whether a self-only-linking file is an
125            // orphan). It is neither incoming nor broken.
126            if target == &file.node_id {
127                continue;
128            }
129            if existing_nodes.contains(target) {
130                linked_to.insert(target.clone());
131            } else if target_resolves_on_disk(store, target) {
132                // A link to an existing non-`.md` source artifact (a `.eml`,
133                // `.pdf`, …) is a live edge, not a broken one — `sources/` holds
134                // such files by design and `graph` resolves them on disk. The
135                // target has no `.md` node, so it can't be `linked_to` (no `.md`
136                // file is un-orphaned by it), but it must NOT be counted broken.
137            } else {
138                // Broken links count occurrences, not distinct targets.
139                stats.broken_link_count += 1;
140            }
141        }
142    }
143
144    // Third pass: roll the per-file facts up into the aggregate Stats. A file is
145    // an orphan iff it has neither a resolvable outgoing edge nor an incoming one.
146    for file in &facts {
147        stats.total_files += 1;
148        *stats.files_per_layer.entry(file.layer).or_insert(0) += 1;
149        stats.total_size_bytes += file.size_bytes;
150
151        if let Some(t) = &file.type_ {
152            *stats.type_distribution.entry(t.clone()).or_insert(0) += 1;
153        }
154
155        let has_outgoing = file.resolvable_targets().any(|t| {
156            t != &file.node_id && (existing_nodes.contains(t) || target_resolves_on_disk(store, t))
157        });
158        let has_incoming = linked_to.contains(&file.node_id);
159        if !has_outgoing && !has_incoming {
160            stats.orphan_count += 1;
161        }
162    }
163
164    stats.top_types = top_types(&stats.type_distribution, TOP_TYPES_LIMIT);
165
166    Ok(stats)
167}
168
169/// The wiki-link matcher: `[[target]]` or `[[target|display]]`. Captures the
170/// target (group 1), excluding `]` and `|`. Anchored on the literal brackets so
171/// it ignores `[markdown](links)`.
172fn wiki_link_regex() -> Regex {
173    // `[^\[\]|]+` keeps the target free of brackets and the display pipe.
174    Regex::new(r"\[\[([^\[\]|]+)(?:\|[^\]]*)?\]\]").expect("static wiki-link regex is valid")
175}
176
177/// Every wiki-link target in a file (frontmatter + body), trimmed, with any
178/// trailing `.md` removed. Order-preserving (frontmatter targets first, then
179/// body); not deduped. stats deliberately counts links in BOTH regions as edges.
180///
181/// The frontmatter block and the body are scanned **separately** so fenced-code
182/// state can never leak between them. YAML frontmatter has no markdown code
183/// fences, so every `[[...]]` there is a real edge and is extracted with no
184/// fence tracking. The body is scanned with fresh fence tracking (started from
185/// no open fence), so a `[[...]]` that lives only inside a body code fence is
186/// still ignored — it is illustrative syntax, not a graph edge, mirroring
187/// `validate::extract_wiki_links` / `store::extract_edge_targets`.
188///
189/// The old single-pass-over-whole-file scan wrongly assumed "frontmatter never
190/// carries code fences": a stray ``` (or `~~~`) line inside a frontmatter value
191/// (e.g. a block-scalar field) opened a fence that swallowed every subsequent
192/// body `[[...]]`, dropping real edges and mis-marking files as orphans.
193fn extract_link_targets(text: &str, re: &Regex) -> Vec<PathBuf> {
194    let (frontmatter, body) = split_frontmatter_and_body(text);
195    let mut out = Vec::new();
196    // (a) Frontmatter: every `[[...]]` is a real edge — no fence tracking. YAML
197    // has no markdown code fences, so a ``` line here is just text, never a fence.
198    if let Some(fm) = frontmatter {
199        for line in fm.lines() {
200            collect_links_on_line(line, re, &mut out);
201        }
202    }
203    // (b) Body: fence-aware, started fresh so no state is inherited from the
204    // frontmatter block above. Track the open fence as `(fence byte, run length)`,
205    // not a single boolean: an inner fence of the *other* character (a `~~~` line
206    // inside an open ``` block, or vice versa) — or a shorter run — is content,
207    // and must NOT close the block. A naive toggle inverts the fence state on
208    // such a line and then mis-classifies every link for the rest of the body.
209    // Mirrors `render`'s `opening_fence` / `is_closing_fence`.
210    let mut fence: Option<(u8, usize)> = None;
211    for line in body.lines() {
212        let content = line.trim_end_matches(['\n', '\r']);
213        if let Some(f) = fence {
214            if is_closing_fence(content, f) {
215                fence = None;
216            }
217            continue;
218        }
219        if let Some(opened) = opening_fence(content) {
220            fence = Some(opened);
221            continue;
222        }
223        collect_links_on_line(line, re, &mut out);
224    }
225    out
226}
227
228/// Push every wiki-link target found on one line into `out` (trimmed,
229/// `.md`-stripped). Shared by the frontmatter and body scans in
230/// [`extract_link_targets`].
231fn collect_links_on_line(line: &str, re: &Regex, out: &mut Vec<PathBuf>) {
232    for cap in re.captures_iter(line) {
233        if let Some(m) = cap.get(1) {
234            let raw = m.as_str().trim();
235            out.push(strip_md(Path::new(raw)));
236        }
237    }
238}
239
240/// Split a file into `(frontmatter YAML, body)`. The frontmatter is the text
241/// between a leading `---` line (the very first line, the universal frontmatter
242/// contract) and its closing `---`; the body is everything after that closing
243/// fence. A file with no valid leading frontmatter block yields `(None, text)` —
244/// the whole text is body — so files with no frontmatter (and a literal `---`
245/// that is content, not a fence) keep their prior whole-file body scan.
246///
247/// Operates on byte offsets into the original `text` so both returned slices
248/// borrow it; the frontmatter slice is offset-equivalent to
249/// [`frontmatter_block`]'s string, and the body picks up immediately after the
250/// closing `---` line's newline.
251fn split_frontmatter_and_body(text: &str) -> (Option<&str>, &str) {
252    // Normalize away a leading BOM, but require `---` as the first line.
253    let stripped = text.strip_prefix('\u{feff}').unwrap_or(text);
254    // The opening fence must be the very first line: a line whose trimmed form is
255    // `---` (trailing whitespace tolerated, matching the prior `frontmatter_block`
256    // and the universal contract), followed by a line break (or EOF). A bare
257    // `---` body line that isn't the first line is not frontmatter.
258    let (first_line, after_first) = match stripped.find('\n') {
259        Some(nl) => (&stripped[..nl], &stripped[nl + 1..]),
260        None => (stripped, ""),
261    };
262    if first_line.trim_end_matches('\r').trim_end() != "---" {
263        return (None, text);
264    }
265    let after_open = after_first;
266    // Scan lines from just after the opening fence for the closing `---`.
267    let mut cursor = after_open;
268    let fm_start = after_open;
269    loop {
270        let (line, tail, had_newline) = match cursor.find('\n') {
271            Some(nl) => (&cursor[..nl], &cursor[nl + 1..], true),
272            None => (cursor, "", false),
273        };
274        if line.trim_end_matches('\r').trim_end() == "---" {
275            // Frontmatter is everything from `fm_start` up to (not including)
276            // this closing `---` line; the body is everything after it.
277            let fm_len = line_offset(fm_start, line);
278            return (Some(&fm_start[..fm_len]), tail);
279        }
280        if !had_newline {
281            // Reached EOF with no closing fence: not a valid frontmatter block.
282            return (None, text);
283        }
284        cursor = tail;
285    }
286}
287
288/// Byte offset of `line` within `base` (both borrow the same buffer). Used to
289/// recover the length of the frontmatter span from its first line and the
290/// closing-fence line.
291fn line_offset(base: &str, line: &str) -> usize {
292    line.as_ptr() as usize - base.as_ptr() as usize
293}
294
295/// If `line` opens a fenced code block, return its `(fence byte, run length)`.
296/// A fence is at least three backticks or tildes, with up to three leading
297/// spaces of indentation. Mirrors `render::opening_fence`.
298fn opening_fence(line: &str) -> Option<(u8, usize)> {
299    let indent = line.len() - line.trim_start_matches(' ').len();
300    if indent > 3 {
301        return None;
302    }
303    let rest = &line[indent..];
304    let byte = rest.bytes().next()?;
305    if byte != b'`' && byte != b'~' {
306        return None;
307    }
308    let run = rest.len() - rest.trim_start_matches(byte as char).len();
309    if run < 3 {
310        return None;
311    }
312    // A backtick fence's info string may not itself contain a backtick.
313    if byte == b'`' && rest[run..].contains('`') {
314        return None;
315    }
316    Some((byte, run))
317}
318
319/// True if `line` closes the currently open fence `(byte, len)`: same fence
320/// char, a run at least as long, and nothing else but trailing whitespace.
321/// Mirrors `render::is_closing_fence`.
322fn is_closing_fence(line: &str, fence: (u8, usize)) -> bool {
323    let (byte, open_len) = fence;
324    let indent = line.len() - line.trim_start_matches(' ').len();
325    if indent > 3 {
326        return false;
327    }
328    let rest = &line[indent..];
329    let run = rest.len() - rest.trim_start_matches(byte as char).len();
330    if run < open_len {
331        return false;
332    }
333    rest[run..].trim().is_empty()
334}
335
336/// Drop a trailing `.md` from a path, leaving everything else intact.
337fn strip_md(path: &Path) -> PathBuf {
338    let s = path.to_string_lossy();
339    match s.strip_suffix(".md") {
340        Some(stem) => PathBuf::from(stem),
341        None => path.to_path_buf(),
342    }
343}
344
345/// True if a wiki-link target is a full store-relative path: it has a path
346/// separator AND its first segment is a recognized layer (`sources`/`records`/
347/// `wiki`) with a non-empty remainder. Short-form targets like `sarah-chen`
348/// are false, and so are non-layer multi-segment targets like
349/// `contacts/sarah-chen` (a missing layer prefix). Doctrine: only true
350/// store-relative paths resolve to a node.
351///
352/// This mirrors `validate::is_full_store_path` so `stats.broken_link_count`
353/// agrees with `validate`'s `WIKI_LINK_BROKEN` total: a non-layer target like
354/// `[[contacts/sarah]]` is a short-form error in `validate` (never broken), and
355/// must likewise be excluded here rather than counted as a broken edge.
356fn is_full_path(target: &Path) -> bool {
357    let mut parts = target.components();
358    let first = match parts.next() {
359        Some(std::path::Component::Normal(s)) => s.to_string_lossy(),
360        _ => return false,
361    };
362    let has_rest = parts.next().is_some();
363    matches!(first.as_ref(), "sources" | "records") && has_rest
364}
365
366/// True if `target` stays inside the store: every component is `Normal` (a
367/// `CurDir` `.` is harmless and allowed), with no `..` (`ParentDir`), absolute
368/// (`RootDir`), or platform-prefix component. Mirrors
369/// `graph::is_within_store_target` and validate's `is_safe_store_relative_path`,
370/// so the containment decision is identical across the three surfaces. Used to
371/// gate any on-disk probe in [`target_resolves_on_disk`] before a `join`.
372fn is_within_store_target(target: &Path) -> bool {
373    target.components().all(|c| {
374        matches!(
375            c,
376            std::path::Component::Normal(_) | std::path::Component::CurDir
377        )
378    })
379}
380
381/// True if a full-path wiki-link `target` (already `.md`-stripped, store-
382/// relative) resolves to a real **non-`.md`** file on disk — a source artifact
383/// like a `.eml` or `.pdf` under `sources/`. Called only after the `.md` node
384/// set has already been checked, so this exists to reconcile stats with `graph`
385/// (which resolves on disk) and `validate`: a link to an existing source file
386/// is a live edge, never a broken link or an orphan-maker.
387///
388/// Two on-disk shapes are recognized, mirroring `graph::resolve_existing` plus
389/// the bare-stem case sources use:
390///
391/// - the target as written is itself a real file (`[[sources/emails/msg.eml]]`
392///   → `sources/emails/msg.eml`);
393/// - the target is a bare stem and a sibling file shares that stem with a
394///   non-`.md` extension (`[[sources/emails/msg]]` → `sources/emails/msg.eml`).
395///
396/// A bare `.md` target is *not* handled here (an existing `.md` file is already
397/// a node in `existing_nodes`); this is strictly the non-`.md` source case.
398///
399/// **Containment gate.** A target that escapes the store root (any `..`,
400/// absolute, or platform-prefix component) is never probed: it returns `false`
401/// before any `join`/`is_file`/`read_dir`, so `[[sources/../../secret]]` can
402/// never reach the filesystem as a live edge or existence oracle outside the
403/// store. This mirrors `graph::is_within_store_target` and validate's
404/// `is_safe_store_relative_path` (which reject `..` before any probe), keeping
405/// the broken-link surface in agreement: an escaping target is counted broken
406/// (validate's `WIKI_LINK_BROKEN`), never silently treated as resolved.
407fn target_resolves_on_disk(store: &Store, target: &Path) -> bool {
408    // Reject any non-`Normal` component (`..`, RootDir, Prefix) up front — never
409    // let a wiki-link turn a stats probe into a filesystem escape.
410    if !is_within_store_target(target) {
411        return false;
412    }
413    // The target as written points at a real file (e.g. an explicit `.eml`).
414    if store.open_regular(target).is_ok() {
415        return true;
416    }
417    // Bare-stem case: look for a sibling `<stem>.<ext>` with a non-`.md`
418    // extension in the target's parent directory. Restricted to the bare form
419    // (no extension on the target) so an explicit but missing `.pdf` link still
420    // reads as broken rather than silently matching a different file.
421    if target.extension().is_some() {
422        return false;
423    }
424    let stem = match target.file_name() {
425        Some(name) => name,
426        None => return false,
427    };
428    let parent = match target.parent() {
429        Some(p) => p,
430        None => return false,
431    };
432    let entries = match store.regular_file_names(parent) {
433        Ok(e) => e,
434        Err(_) => return false,
435    };
436    for name in entries {
437        let path = Path::new(&name);
438        // Same stem, and an extension that is present and not `.md`.
439        if path.file_stem() == Some(stem) {
440            match path.extension().and_then(|e| e.to_str()) {
441                Some("md") | None => continue,
442                Some(_) => return true,
443            }
444        }
445    }
446    false
447}
448
449/// Read the `type:` value from a file's leading YAML frontmatter block, if the
450/// file has one. Returns `None` when there's no frontmatter or no `type` key.
451/// Self-contained (does not route through the crate's parser): split on the
452/// `---` fences, parse the block as a YAML mapping, read `type` as a string.
453fn parse_type(text: &str) -> Option<String> {
454    let yaml = frontmatter_block(text)?;
455    let value: serde_norway::Value = serde_norway::from_str(&yaml).ok()?;
456    let mapping = value.as_mapping()?;
457    let type_val = mapping.get(serde_norway::Value::String("type".to_string()))?;
458    let s = type_val.as_str()?.trim();
459    if s.is_empty() {
460        None
461    } else {
462        Some(s.to_string())
463    }
464}
465
466/// Extract the raw YAML between a leading `---` fence and its closing `---`.
467/// The opening fence must be the very first line of the file (the universal
468/// frontmatter contract: frontmatter is the first thing in the file). Delegates
469/// to [`split_frontmatter_and_body`] so the frontmatter boundary is computed in
470/// exactly one place (the type-parse and the link-scan never disagree on where
471/// frontmatter ends).
472fn frontmatter_block(text: &str) -> Option<String> {
473    split_frontmatter_and_body(text).0.map(str::to_string)
474}
475
476/// Sort a type distribution into the top `limit` types by count descending,
477/// ties broken by type name ascending.
478fn top_types(dist: &BTreeMap<String, usize>, limit: usize) -> Vec<(String, usize)> {
479    let mut pairs: Vec<(String, usize)> = dist.iter().map(|(k, v)| (k.clone(), *v)).collect();
480    // BTreeMap iteration is already name-ascending; a stable sort by count
481    // descending therefore yields (count desc, name asc).
482    pairs.sort_by_key(|p| std::cmp::Reverse(p.1));
483    pairs.truncate(limit);
484    pairs
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::parser::Config;
491    use std::fs;
492    use tempfile::TempDir;
493
494    /// Build a `Store` rooted at a fresh tempdir with an empty `DB.md` marker.
495    /// Bypasses `Store::open` by constructing the struct directly —
496    /// `stats::compute` only reads `store.root`.
497    fn temp_store() -> (TempDir, Store) {
498        let dir = TempDir::new().expect("tempdir");
499        fs::write(dir.path().join("DB.md"), "---\ntype: db-md\n---\n").expect("write DB.md");
500        let store = Store::from_root_and_config(dir.path(), Config::default()).unwrap();
501        (dir, store)
502    }
503
504    /// Like [`temp_store`], but roots the store one level *inside* the tempdir
505    /// (`<tempdir>/store`) so `store.root.parent()` is the test's own private
506    /// tempdir rather than the shared OS temp root. Tests that plant a file
507    /// "above the store root" must use this — writing into `store.root.parent()`
508    /// of a top-level `TempDir` lands in `$TMPDIR`, which is shared across every
509    /// parallel test (and across test binaries under `cargo test --workspace`),
510    /// so two such tests collide on the same path and race.
511    fn temp_store_nested() -> (TempDir, Store) {
512        let dir = TempDir::new().expect("tempdir");
513        let root = dir.path().join("store");
514        fs::create_dir_all(&root).expect("create store root");
515        fs::write(root.join("DB.md"), "---\ntype: db-md\n---\n").expect("write DB.md");
516        let store = Store::from_root_and_config(&root, Config::default()).unwrap();
517        (dir, store)
518    }
519
520    /// Write a content file at a store-relative path, creating parent dirs.
521    fn write_rel(store: &Store, rel: &str, contents: &str) {
522        let abs = store.root.join(rel);
523        if let Some(parent) = abs.parent() {
524            fs::create_dir_all(parent).expect("mkdir parents");
525        }
526        fs::write(abs, contents).expect("write content file");
527    }
528
529    /// A minimal content file body: frontmatter with the given type, no links.
530    fn doc(type_: &str, summary: &str) -> String {
531        format!("---\ntype: {type_}\nsummary: \"{summary}\"\n---\n\nbody\n")
532    }
533
534    #[test]
535    fn empty_store_is_all_zeros() {
536        let (_d, store) = temp_store();
537        let s = compute(&store).expect("compute");
538        assert_eq!(s.total_files, 0);
539        assert_eq!(s.total_size_bytes, 0);
540        assert!(s.files_per_layer.is_empty());
541        assert!(s.type_distribution.is_empty());
542        assert_eq!(s.orphan_count, 0);
543        assert_eq!(s.broken_link_count, 0);
544        assert!(s.top_types.is_empty());
545    }
546
547    #[test]
548    fn counts_files_per_layer_and_total() {
549        let (_d, store) = temp_store();
550        write_rel(&store, "sources/emails/a.md", &doc("email", "a"));
551        write_rel(&store, "sources/emails/b.md", &doc("email", "b"));
552        write_rel(&store, "records/contacts/c.md", &doc("contact", "c"));
553        // A conclusion record (former wiki-page) lives in the records layer.
554        write_rel(&store, "records/profiles/p.md", &doc("profile", "p"));
555
556        let s = compute(&store).expect("compute");
557        assert_eq!(s.total_files, 4);
558        assert_eq!(s.files_per_layer.get(&Layer::Sources), Some(&2));
559        assert_eq!(s.files_per_layer.get(&Layer::Records), Some(&2));
560    }
561
562    #[test]
563    fn ignores_meta_files_and_non_md_and_dotdirs_and_log() {
564        let (_d, store) = temp_store();
565        // Real content.
566        write_rel(&store, "records/contacts/real.md", &doc("contact", "real"));
567        // Meta + non-content that must NOT be counted.
568        write_rel(
569            &store,
570            "records/contacts/index.md",
571            "---\ntype: index\nscope: type-folder\n---\n",
572        );
573        write_rel(&store, "records/contacts/index.jsonl", "{}\n");
574        write_rel(&store, "records/notes.txt", "not markdown\n");
575        // `log/` archive tree under a layer is skipped wholesale.
576        write_rel(&store, "sources/log/2026-04.md", &doc("email", "archived"));
577        // Hidden dir contents are skipped.
578        write_rel(
579            &store,
580            "records/.obsidian/cache.md",
581            &doc("profile", "hidden"),
582        );
583
584        let s = compute(&store).expect("compute");
585        assert_eq!(s.total_files, 1, "only the one real content file counts");
586        assert_eq!(s.files_per_layer.get(&Layer::Records), Some(&1));
587        assert_eq!(s.files_per_layer.get(&Layer::Sources), None);
588    }
589
590    #[test]
591    fn total_size_is_sum_of_content_file_bytes() {
592        let (_d, store) = temp_store();
593        let a = doc("email", "a");
594        let b = "---\ntype: contact\nsummary: x\n---\n\nlonger body text here\n".to_string();
595        write_rel(&store, "sources/emails/a.md", &a);
596        write_rel(&store, "records/contacts/b.md", &b);
597        // A skipped file's bytes must not be included.
598        write_rel(
599            &store,
600            "records/contacts/index.md",
601            "---\ntype: index\n---\nbig meta file padding padding\n",
602        );
603
604        let s = compute(&store).expect("compute");
605        let expected = a.len() as u64 + b.len() as u64;
606        assert_eq!(s.total_size_bytes, expected);
607    }
608
609    #[test]
610    fn type_distribution_counts_each_type_value() {
611        let (_d, store) = temp_store();
612        write_rel(&store, "sources/emails/a.md", &doc("email", "a"));
613        write_rel(&store, "sources/emails/b.md", &doc("email", "b"));
614        write_rel(&store, "sources/emails/c.md", &doc("email", "c"));
615        write_rel(&store, "records/contacts/d.md", &doc("contact", "d"));
616        write_rel(&store, "records/proposals/e.md", &doc("proposal", "e"));
617
618        let s = compute(&store).expect("compute");
619        assert_eq!(s.type_distribution.get("email"), Some(&3));
620        assert_eq!(s.type_distribution.get("contact"), Some(&1));
621        assert_eq!(s.type_distribution.get("proposal"), Some(&1));
622        assert_eq!(s.type_distribution.len(), 3);
623    }
624
625    #[test]
626    fn file_without_type_is_counted_in_totals_but_not_distribution() {
627        let (_d, store) = temp_store();
628        // A content file with frontmatter but no `type:` key.
629        write_rel(
630            &store,
631            "records/themes/x.md",
632            "---\nsummary: no type here\n---\n\nbody\n",
633        );
634        // A content file with no frontmatter at all.
635        write_rel(
636            &store,
637            "records/themes/y.md",
638            "just a body, no frontmatter\n",
639        );
640
641        let s = compute(&store).expect("compute");
642        assert_eq!(s.total_files, 2, "untyped files still count toward totals");
643        assert_eq!(s.files_per_layer.get(&Layer::Records), Some(&2));
644        assert!(
645            s.type_distribution.is_empty(),
646            "no type key => no distribution entry, not an empty-string bucket"
647        );
648    }
649
650    #[test]
651    fn top_types_orders_by_count_desc_then_name_asc() {
652        let (_d, store) = temp_store();
653        // contact x3, email x3 (tie), decision x1.
654        write_rel(&store, "records/contacts/c1.md", &doc("contact", "1"));
655        write_rel(&store, "records/contacts/c2.md", &doc("contact", "2"));
656        write_rel(&store, "records/contacts/c3.md", &doc("contact", "3"));
657        write_rel(&store, "sources/emails/e1.md", &doc("email", "1"));
658        write_rel(&store, "sources/emails/e2.md", &doc("email", "2"));
659        write_rel(&store, "sources/emails/e3.md", &doc("email", "3"));
660        write_rel(&store, "records/decisions/d1.md", &doc("decision", "1"));
661
662        let s = compute(&store).expect("compute");
663        assert_eq!(
664            s.top_types,
665            vec![
666                ("contact".to_string(), 3),
667                ("email".to_string(), 3),
668                ("decision".to_string(), 1),
669            ],
670            "ties (contact, email both 3) break by name ascending; decision trails"
671        );
672    }
673
674    #[test]
675    fn top_types_is_capped_at_ten() {
676        let (_d, store) = temp_store();
677        // 12 distinct custom types, each one file.
678        for i in 0..12 {
679            let t = format!("type{i:02}");
680            write_rel(&store, &format!("records/{t}/f.md"), &doc(&t, "x"));
681        }
682        let s = compute(&store).expect("compute");
683        assert_eq!(s.top_types.len(), 10, "top_types caps at 10");
684        assert_eq!(
685            s.type_distribution.len(),
686            12,
687            "distribution keeps all types"
688        );
689    }
690
691    #[test]
692    fn orphans_are_files_with_no_incoming_and_no_outgoing_links() {
693        let (_d, store) = temp_store();
694        // a -> b (a has outgoing, b has incoming). c is isolated => orphan.
695        write_rel(
696            &store,
697            "records/contacts/a.md",
698            "---\ntype: contact\nsummary: a\n---\n\nSee [[records/contacts/b]].\n",
699        );
700        write_rel(&store, "records/contacts/b.md", &doc("contact", "b"));
701        write_rel(&store, "records/contacts/c.md", &doc("contact", "c"));
702
703        let s = compute(&store).expect("compute");
704        assert_eq!(s.orphan_count, 1, "only c is an orphan");
705    }
706
707    #[test]
708    fn a_file_with_only_a_self_link_is_an_orphan_matching_graph() {
709        let (_d, store) = temp_store();
710        // A file that links only to ITSELF has no real graph edge, so it must be
711        // an orphan — consistent with `graph::orphans` (which skips self-links).
712        write_rel(
713            &store,
714            "records/contacts/solo.md",
715            "---\ntype: contact\nsummary: solo\n---\n\nSee [[records/contacts/solo]].\n",
716        );
717        let s = compute(&store).expect("compute");
718        assert_eq!(
719            s.orphan_count, 1,
720            "a self-only-linking file is an orphan: {s:?}"
721        );
722    }
723
724    #[test]
725    fn a_file_with_only_an_incoming_link_is_not_an_orphan() {
726        let (_d, store) = temp_store();
727        // b has no outgoing links, but a links to it => b is NOT an orphan.
728        // a itself has an outgoing link => also not an orphan. Zero orphans.
729        write_rel(
730            &store,
731            "records/profiles/a.md",
732            "---\ntype: profile\nsummary: a\n---\n\n[[records/profiles/b]]\n",
733        );
734        write_rel(&store, "records/profiles/b.md", &doc("profile", "b"));
735
736        let s = compute(&store).expect("compute");
737        assert_eq!(s.orphan_count, 0);
738    }
739
740    #[test]
741    fn frontmatter_wiki_links_count_as_edges_for_orphans() {
742        let (_d, store) = temp_store();
743        // The link lives in a frontmatter field, not the body. It must still
744        // wire `contact` -> `company`, so neither is an orphan.
745        write_rel(
746            &store,
747            "records/contacts/sarah.md",
748            "---\ntype: contact\nsummary: s\ncompany: [[records/companies/acme]]\n---\n\nbody\n",
749        );
750        write_rel(&store, "records/companies/acme.md", &doc("company", "acme"));
751
752        let s = compute(&store).expect("compute");
753        assert_eq!(
754            s.orphan_count, 0,
755            "a frontmatter wiki-link is a real edge; neither endpoint is orphaned"
756        );
757    }
758
759    #[test]
760    fn broken_links_count_targets_that_do_not_exist() {
761        let (_d, store) = temp_store();
762        // Two links: one to an existing file, one to a missing file.
763        write_rel(
764            &store,
765            "records/profiles/a.md",
766            "---\ntype: profile\nsummary: a\n---\n\n[[records/profiles/b]] and [[records/contacts/ghost]]\n",
767        );
768        write_rel(&store, "records/profiles/b.md", &doc("profile", "b"));
769
770        let s = compute(&store).expect("compute");
771        assert_eq!(s.broken_link_count, 1, "only the ghost target is broken");
772    }
773
774    #[test]
775    fn broken_link_resolves_with_md_extension_stripped() {
776        let (_d, store) = temp_store();
777        // Link written WITH a `.md` extension still resolves to the real file
778        // (the parser accepts `.md`; validate only warns). Not broken.
779        write_rel(
780            &store,
781            "records/profiles/a.md",
782            "---\ntype: profile\nsummary: a\n---\n\n[[records/profiles/b.md]]\n",
783        );
784        write_rel(&store, "records/profiles/b.md", &doc("profile", "b"));
785
786        let s = compute(&store).expect("compute");
787        assert_eq!(
788            s.broken_link_count, 0,
789            "a `.md`-suffixed target resolves to the same node and is not broken"
790        );
791    }
792
793    #[test]
794    fn short_form_links_are_not_broken_and_do_not_wire_the_graph() {
795        let (_d, store) = temp_store();
796        // `[[b]]` is a short-form (no `/`): a validation error elsewhere, but
797        // for stats it neither counts as broken (it doesn't resolve to a node)
798        // nor wires `a` into the graph. So `a` (no other links) is an orphan.
799        write_rel(
800            &store,
801            "records/contacts/a.md",
802            "---\ntype: contact\nsummary: a\n---\n\n[[b]]\n",
803        );
804        write_rel(&store, "records/contacts/b.md", &doc("contact", "b"));
805
806        let s = compute(&store).expect("compute");
807        assert_eq!(
808            s.broken_link_count, 0,
809            "short-form links are not counted as broken by stats"
810        );
811        // a has only a short-form link (not an edge) => orphan. b has no links
812        // and no real incoming edge => orphan. Both orphaned.
813        assert_eq!(s.orphan_count, 2);
814    }
815
816    #[test]
817    fn display_alias_links_resolve_to_the_target_not_the_alias() {
818        let (_d, store) = temp_store();
819        // `[[records/profiles/b|Bob]]` targets b, displays "Bob". The alias must
820        // be stripped: the edge goes to b (exists), so it's not broken and b is
821        // not an orphan.
822        write_rel(
823            &store,
824            "records/profiles/a.md",
825            "---\ntype: profile\nsummary: a\n---\n\nmet [[records/profiles/b|Bob]] today\n",
826        );
827        write_rel(&store, "records/profiles/b.md", &doc("profile", "b"));
828
829        let s = compute(&store).expect("compute");
830        assert_eq!(s.broken_link_count, 0, "alias target resolves and exists");
831        assert_eq!(s.orphan_count, 0, "a links out, b is linked to");
832    }
833
834    #[test]
835    fn duplicate_links_in_one_file_count_broken_per_occurrence() {
836        let (_d, store) = temp_store();
837        // The same missing target twice => two broken-link occurrences.
838        write_rel(
839            &store,
840            "records/profiles/a.md",
841            "---\ntype: profile\nsummary: a\n---\n\n[[records/contacts/ghost]] [[records/contacts/ghost]]\n",
842        );
843        let s = compute(&store).expect("compute");
844        assert_eq!(
845            s.broken_link_count, 2,
846            "broken links count occurrences, not distinct targets"
847        );
848    }
849
850    #[test]
851    fn markdown_links_are_not_treated_as_wiki_links() {
852        let (_d, store) = temp_store();
853        // A standard markdown link to an external URL must not register as a
854        // wiki edge (so this file stays an orphan) nor as a broken link.
855        write_rel(
856            &store,
857            "records/profiles/a.md",
858            "---\ntype: profile\nsummary: a\n---\n\nSee [Acme](https://acme.io/path).\n",
859        );
860        let s = compute(&store).expect("compute");
861        assert_eq!(s.broken_link_count, 0, "markdown links aren't graph edges");
862        assert_eq!(s.orphan_count, 1, "the file has no wiki-links => orphan");
863    }
864
865    #[test]
866    fn regression_non_layer_multi_segment_link_is_not_broken() {
867        // Finding #20: a target like `[[contacts/sarah-chen]]` omits the layer
868        // prefix. It has a `/` but its first segment (`contacts`) is not a
869        // recognized layer, so it's a short-form error in `validate`, NOT a
870        // broken link. stats must agree: it counts neither as broken nor as an
871        // outgoing edge. Pre-fix `is_full_path` (components().count() > 1)
872        // accepted it and reported broken_link_count = 1.
873        let (_d, store) = temp_store();
874        write_rel(
875            &store,
876            "records/contacts/a.md",
877            "---\ntype: contact\nsummary: a\n---\n\nSee [[contacts/sarah-chen]].\n",
878        );
879        let s = compute(&store).expect("compute");
880        assert_eq!(
881            s.broken_link_count, 0,
882            "a non-layer multi-segment target is a short-form error, not broken"
883        );
884        // The non-layer link is not a graph edge, so `a` has no outgoing edge
885        // and is an orphan — matching how validate/graph treat it.
886        assert_eq!(
887            s.orphan_count, 1,
888            "the non-layer link does not wire `a` out of orphan status"
889        );
890    }
891
892    #[test]
893    fn regression_wiki_links_in_code_fences_are_ignored() {
894        // Finding #21: a wiki-link that appears only inside a fenced code block
895        // is illustrative syntax, not a graph edge. validate skips fenced
896        // regions; stats must too. Pre-fix the regex ran over the whole file
897        // with no fence tracking, so the fenced ghost link inflated
898        // broken_link_count to 1 and the fenced real link un-orphaned the page.
899        let (_d, store) = temp_store();
900        // A howto page whose ONLY wiki-links live inside ``` and ~~~ fences:
901        // one to a missing target, one to an existing target.
902        write_rel(
903            &store,
904            "records/synthesis/howto.md",
905            "---\ntype: synthesis\nsummary: howto\n---\n\
906             \nWrite links like this:\n\
907             \n```\n[[records/contacts/ghost]]\n```\n\
908             \nor this:\n\
909             \n~~~\n[[records/synthesis/real]]\n~~~\n",
910        );
911        write_rel(
912            &store,
913            "records/synthesis/real.md",
914            &doc("synthesis", "real"),
915        );
916        let s = compute(&store).expect("compute");
917        assert_eq!(
918            s.broken_link_count, 0,
919            "a `[[...]]` inside a code fence is not a real (broken) edge"
920        );
921        // howto has no real edges => orphan. real is not linked-to by any real
922        // edge => orphan. Both orphaned (2), proving the fenced link to `real`
923        // did not wire either file out of orphan status.
924        assert_eq!(
925            s.orphan_count, 2,
926            "fenced wiki-links do not wire files out of orphan status: {s:?}"
927        );
928    }
929
930    #[test]
931    fn a_link_to_an_existing_file_in_another_layer_resolves() {
932        let (_d, store) = temp_store();
933        // A records-layer profile links to a source file in the other layer;
934        // cross-layer full-path links resolve like any other.
935        write_rel(
936            &store,
937            "records/profiles/a.md",
938            "---\ntype: profile\nsummary: a\n---\n\nfrom [[sources/emails/2026/05/m]]\n",
939        );
940        write_rel(&store, "sources/emails/2026/05/m.md", &doc("email", "m"));
941
942        let s = compute(&store).expect("compute");
943        assert_eq!(s.broken_link_count, 0);
944        assert_eq!(s.orphan_count, 0, "both endpoints are wired");
945    }
946
947    #[test]
948    fn regression_tilde_line_inside_backtick_fence_does_not_invert_state() {
949        // Finding #44/#11: a `~~~` line inside an open ``` fence (or any inner
950        // fence of the other char / a shorter run) must NOT close the block.
951        // Pre-fix a single boolean toggled on it, inverting fence state so the
952        // fenced ghost link counted broken and the real link after the fence
953        // was dropped. With (byte, run-length) tracking the block only closes on
954        // a matching ``` fence.
955        let (_d, store) = temp_store();
956        write_rel(&store, "records/profiles/bob.md", &doc("profile", "bob"));
957        // ```text … ~~~ x (inner tilde line) … [[ghost]] … ``` then a real link.
958        write_rel(
959            &store,
960            "records/concepts/howto.md",
961            "---\ntype: concept\nsummary: howto\n---\n\
962             \n```text\n~~~ x\n[[records/profiles/ghost]]\n```\n\
963             \nReal: [[records/profiles/bob]]\n",
964        );
965
966        let s = compute(&store).expect("compute");
967        assert_eq!(
968            s.broken_link_count, 0,
969            "the fenced ghost link is inside the unbroken ``` block, not broken: {s:?}"
970        );
971        // bob is linked from howto (a real edge after the fence closes), and
972        // howto links out — neither is an orphan.
973        assert_eq!(
974            s.orphan_count, 0,
975            "the real post-fence link wires both files: {s:?}"
976        );
977    }
978
979    #[test]
980    fn regression_frontmatter_code_fence_does_not_swallow_body_links() {
981        // A stray code-fence line INSIDE the frontmatter (here an unbalanced ```
982        // in a YAML block-scalar value) must not leak fenced-code state into the
983        // body scan. Pre-fix `extract_link_targets` scanned the whole file with a
984        // single fence tracker, so the frontmatter ``` opened a fence that
985        // swallowed every later body `[[...]]`, dropping the real edge and
986        // mis-marking both endpoints as orphans. The fix splits frontmatter from
987        // body and starts the body fence tracking fresh.
988        let (_d, store) = temp_store();
989        write_rel(
990            &store,
991            "records/contacts/alice.md",
992            &doc("contact", "alice"),
993        );
994        // A profile whose frontmatter carries a wiki-link field AND an unbalanced
995        // ``` line, then a REAL body link plus a genuinely-fenced body link.
996        write_rel(
997            &store,
998            "records/profiles/note.md",
999            "---\ntype: profile\nsummary: note\n\
1000             refs: \"[[records/contacts/alice]]\"\n\
1001             field: |\n  start of a fence\n  ```\n  never closed in frontmatter\n---\n\
1002             \nReal: [[records/contacts/alice]]\n\
1003             \n```\n[[records/contacts/ghost]]\n```\n",
1004        );
1005
1006        let s = compute(&store).expect("compute");
1007        // The frontmatter ``` no longer hides the body link to alice, and the
1008        // body-fenced ghost link is still ignored (not broken). If the leak
1009        // returned, the body link would be dropped and the ghost would surface.
1010        assert_eq!(
1011            s.broken_link_count, 0,
1012            "the genuinely body-fenced ghost link is not a broken edge, \
1013             and the frontmatter fence did not surface it: {s:?}"
1014        );
1015        // alice is linked from note (via both the frontmatter `refs:` edge and
1016        // the real body link), and note links out — neither is an orphan. Pre-fix
1017        // the frontmatter fence dropped the body link and the orphan count was 2.
1018        assert_eq!(
1019            s.orphan_count, 0,
1020            "the body link survives the frontmatter code fence and wires both files: {s:?}"
1021        );
1022    }
1023
1024    #[test]
1025    fn regression_nested_log_directory_is_counted_not_skipped() {
1026        // Finding #45: only the layer's IMMEDIATE `log/` archive is skipped. A
1027        // directory named `log` nested under a type-folder is ordinary content
1028        // and must be counted, matching tree/index/query. Pre-fix any `log` dir
1029        // at any depth was pruned, making the whole subtree invisible to stats.
1030        let (_d, store) = temp_store();
1031        write_rel(
1032            &store,
1033            "sources/emails/log/maillog.md",
1034            &doc(
1035                "email",
1036                "an archived mail log entry under a log subdirectory",
1037            ),
1038        );
1039        // The layer-immediate `log/` archive is still skipped.
1040        write_rel(&store, "sources/log/2026-04.md", &doc("email", "rotated"));
1041
1042        let s = compute(&store).expect("compute");
1043        assert_eq!(
1044            s.total_files, 1,
1045            "the nested sources/emails/log file counts; the layer-immediate sources/log is skipped: {s:?}"
1046        );
1047        assert_eq!(s.files_per_layer.get(&Layer::Sources), Some(&1));
1048        assert_eq!(s.type_distribution.get("email"), Some(&1));
1049    }
1050
1051    #[test]
1052    fn regression_link_to_existing_non_md_source_is_a_live_edge() {
1053        // Finding (high): a record that wiki-links to an existing non-`.md`
1054        // source artifact (a `.eml`) must read as a LIVE edge, not broken, and
1055        // the record is not an orphan. `sources/` holds such files by design.
1056        let (_d, store) = temp_store();
1057        // A real .eml source file (not a .md content file).
1058        write_rel(
1059            &store,
1060            "sources/emails/msg.eml",
1061            "From: someone@example.com\nSubject: Renewal\n\nBody text.\n",
1062        );
1063        // A record with the SPEC-canonical bare link to that source.
1064        write_rel(
1065            &store,
1066            "records/contacts/sarah.md",
1067            "---\ntype: contact\nsummary: s\n---\n\nLinked source: [[sources/emails/msg]]\n",
1068        );
1069
1070        let s = compute(&store).expect("compute");
1071        assert_eq!(
1072            s.broken_link_count, 0,
1073            "a link to an existing .eml source is live, not broken: {s:?}"
1074        );
1075        assert_eq!(
1076            s.orphan_count, 0,
1077            "the linking record has a resolvable outgoing edge to the source: {s:?}"
1078        );
1079        // The explicit-extension form resolves the same way.
1080        write_rel(
1081            &store,
1082            "records/contacts/sarah.md",
1083            "---\ntype: contact\nsummary: s\n---\n\nLinked source: [[sources/emails/msg.eml]]\n",
1084        );
1085        let s2 = compute(&store).expect("compute");
1086        assert_eq!(s2.broken_link_count, 0, "explicit .eml target resolves too");
1087        assert_eq!(s2.orphan_count, 0);
1088    }
1089
1090    #[test]
1091    fn regression_traversal_target_is_broken_not_a_filesystem_escape() {
1092        // SECURITY regression: a `..`-laden wiki-link target must never turn a
1093        // stats probe into a read of a file OUTSIDE the store. Pre-fix
1094        // `target_resolves_on_disk` joined the raw target onto the store root and
1095        // probed `is_file` / `read_dir` with no containment check, so
1096        // `[[sources/../../outside-secret]]` reached a file above the store and
1097        // was silently counted as a LIVE edge (un-orphaning the linker and never
1098        // counted broken) — diverging from validate (which flags it
1099        // WIKI_LINK_BROKEN) and graph (which drops it). The gate now rejects any
1100        // non-`Normal` component before any join, so it counts broken.
1101        // Nested store: `store.root.parent()` is this test's private tempdir,
1102        // never the shared `$TMPDIR` (which the sibling traversal test would also
1103        // write into, racing on the same filename under `--workspace`).
1104        let (_d, store) = temp_store_nested();
1105        // Every store has a `sources/` dir; the traversal needs its first
1106        // component to be a recognized layer to pass `is_full_path`.
1107        fs::create_dir_all(store.root.join("sources/emails")).unwrap();
1108        // Plant a secret ABOVE the store root (the parent of the store dir).
1109        let outside_dir = store.root.parent().expect("store has a parent");
1110        fs::write(outside_dir.join("outside-secret.txt"), "TOP SECRET\n").unwrap();
1111
1112        // Bare-stem traversal (would hit the `read_dir` parent branch) and the
1113        // explicit-extension traversal (would hit the `is_file` literal branch).
1114        for target in [
1115            "sources/../../outside-secret",
1116            "sources/../../outside-secret.txt",
1117        ] {
1118            write_rel(
1119                &store,
1120                "records/contacts/a.md",
1121                &format!("---\ntype: contact\nsummary: s\n---\n\nEscape: [[{target}]]\n"),
1122            );
1123            let s = compute(&store).expect("compute");
1124            assert_eq!(
1125                s.broken_link_count, 1,
1126                "a `..` target escaping the store must be broken, not a live edge ({target}): {s:?}"
1127            );
1128            assert_eq!(
1129                s.orphan_count, 1,
1130                "an escaping link must NOT wire the linker out of orphan status ({target}): {s:?}"
1131            );
1132        }
1133        // The secret outside the store is untouched (we never followed the link).
1134        assert_eq!(
1135            fs::read_to_string(outside_dir.join("outside-secret.txt")).unwrap(),
1136            "TOP SECRET\n"
1137        );
1138    }
1139
1140    #[test]
1141    fn regression_target_resolves_on_disk_rejects_traversal_before_any_probe() {
1142        // SECURITY regression at the helper level: `target_resolves_on_disk`
1143        // must return `false` for any `..`-laden / absolute / prefix target
1144        // BEFORE it joins, `is_file`s, or `read_dir`s — so a wiki-link can never
1145        // turn a stats existence-probe into a read of a file OUTSIDE the store.
1146        // Pre-fix the helper joined the raw target onto the store root with no
1147        // containment gate, so a real file above the store made it return
1148        // `true`. This asserts the gate directly on the helper (the end-to-end
1149        // `compute()` path is covered separately above), exercising BOTH on-disk
1150        // branches: the literal `is_file` branch (explicit extension) and the
1151        // bare-stem `read_dir` branch.
1152        // Nested store: `store.root.parent()` is this test's private tempdir, so
1153        // the "above the store" files below never land in the shared `$TMPDIR`
1154        // and can never collide with the sibling traversal test's identically
1155        // named planted files when both run in parallel.
1156        let (_d, store) = temp_store_nested();
1157        // A real `sources/` tree exists (the literal/parent joins would have
1158        // something to land near), matching a real store.
1159        fs::create_dir_all(store.root.join("sources/emails")).unwrap();
1160        // Plant matching files ABOVE the store root: one with the exact name the
1161        // explicit-extension target points at, and one whose stem the bare-stem
1162        // target would discover via `read_dir` of the (escaped) parent dir.
1163        let outside_dir = store.root.parent().expect("store has a parent");
1164        fs::write(outside_dir.join("outside-secret.txt"), "TOP SECRET\n").unwrap();
1165        fs::write(outside_dir.join("outside-secret.eml"), "secret mail\n").unwrap();
1166
1167        // Explicit-extension traversal -> would hit the literal `is_file` branch.
1168        assert!(
1169            !target_resolves_on_disk(
1170                &store,
1171                &strip_md(Path::new("sources/../../outside-secret.txt"))
1172            ),
1173            "an explicit-extension `..` target escaping the store must not resolve on disk"
1174        );
1175        // Bare-stem traversal -> would hit the `read_dir(parent)` branch, where a
1176        // sibling `outside-secret.eml` (non-`.md`) sits beside the escaped parent.
1177        assert!(
1178            !target_resolves_on_disk(&store, &strip_md(Path::new("sources/../../outside-secret"))),
1179            "a bare-stem `..` target escaping the store must not resolve on disk"
1180        );
1181        // A `..` that stays nominally under a layer prefix is still an escape and
1182        // is rejected before any probe.
1183        assert!(
1184            !target_resolves_on_disk(&store, Path::new("records/../records/secret")),
1185            "any `..` component is rejected before a probe, even one re-entering a layer"
1186        );
1187
1188        // Sanity: a legitimate in-store non-`.md` source DOES still resolve, so
1189        // the gate did not over-reject and break the finding #117 behavior.
1190        write_rel(
1191            &store,
1192            "sources/emails/msg.eml",
1193            "From: a@b.com\nSubject: x\n\nbody\n",
1194        );
1195        assert!(
1196            target_resolves_on_disk(&store, Path::new("sources/emails/msg")),
1197            "a legitimate in-store bare-stem source link still resolves on disk"
1198        );
1199
1200        // The secrets outside the store are untouched (we never followed a link).
1201        assert_eq!(
1202            fs::read_to_string(outside_dir.join("outside-secret.txt")).unwrap(),
1203            "TOP SECRET\n"
1204        );
1205    }
1206
1207    #[test]
1208    fn regression_link_to_truly_missing_source_is_still_broken() {
1209        // Guard the source-resolution fix doesn't over-resolve: a bare link
1210        // whose target has NO file of any extension on disk is still broken.
1211        let (_d, store) = temp_store();
1212        write_rel(
1213            &store,
1214            "records/contacts/sarah.md",
1215            "---\ntype: contact\nsummary: s\n---\n\nLinked: [[sources/emails/missing]]\n",
1216        );
1217        let s = compute(&store).expect("compute");
1218        assert_eq!(
1219            s.broken_link_count, 1,
1220            "a target with no on-disk file in any form is broken: {s:?}"
1221        );
1222    }
1223}