vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
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
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
//! Zero-stubs gate — enforces VYRE_RELEASE_PLAN criterion 17.
//!
//! A **stub** is a lie that the tree is further along than it really
//! is. This gate fails red on any of the following:
//!
//! - `todo!()`, `unimplemented!()`, `unreachable!()` in non-test code.
//! - `// TODO`, `// FIXME`, `// XXX`, `// HACK`, `// STUB` comments in
//!   any source file (test, bench, or production).
//! - An `#[allow(dead_code)]` attribute anywhere under vyre's `src/`
//!   trees — if the item is unreachable, either delete it or wire it
//!   to a real caller; the allow-attribute is forbidden scaffolding.
//! - A function whose entire body is a trivial return of `Vec::new()`,
//!   `vec![]`, `0`, `None`, `""`, `String::new()`, or
//!   `Default::default()` **when the function takes at least one
//!   argument or its signature promises a non-trivial result**. A
//!   no-op is a stub when the caller expects work.
//!
//! Findings are actionable — every finding carries the file, line,
//! stub kind, and a `Fix:` suggestion pointing at the concrete path
//! forward.
//!
//! The gate is wired into the conform `enforce_all` orchestration as
//! the *last* check so a pipeline that passed every other layer still
//! fails if a stub slipped in. This is cheap: a single source walk
//! with linear substring matching plus a quick `syn` parse for the
//! trivial-body detection.
//!
//! # Scope
//!
//! The walker visits every `.rs` file under the paths supplied by the
//! caller. Tests that want to exercise the gate hand it a temporary
//! directory. `enforce_all` hands it the vyre workspace root minus
//! known-stub directories (`target/`, `coordination/`, `docs/`,
//! `target-*/`). There is a per-call `allowlist` for files whose
//! whole-file stubs are tracked outside the gate (e.g. a generated
//! file that is legitimately empty on a clean tree).
//!
//! # Why `enforce_all` does not walk vyre's own workspace
//!
//! vyre is a library, not a CLI — conform's main binary has no
//! `cargo run`-time knowledge of the workspace checkout. The gate
//! defaults to `discovery_roots(workspace_root)` which the conform
//! `build.rs` already captures, and `enforce_all` passes it through.
//! The baseline allowlist is empty; every new waiver is an engine
//! finding, not a gate-weakening hack.

use std::fs;
use std::path::{Path, PathBuf};

use walkdir::WalkDir;

/// A single stub finding emitted by the gate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZeroStubFinding {
    /// Relative path (from the walk root) of the offending file.
    pub file: PathBuf,
    /// 1-indexed line number of the offending token.
    pub line: usize,
    /// Kind of stub detected.
    pub kind: StubKind,
    /// Verbatim offending snippet (truncated to 160 chars).
    pub snippet: String,
    /// Actionable fix suggestion prefixed with `Fix:`.
    pub fix: String,
}

/// All recognized stub categories. Keep this enum `non_exhaustive`
/// so new categories can be added without breaking the
/// `match`-based reporter in `enforce_all`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StubKind {
    /// `todo!()` / `todo!("...")` macro call in non-test code.
    TodoMacro,
    /// `unimplemented!()` macro call in non-test code.
    UnimplementedMacro,
    /// `unreachable!()` in non-test code. Unreachable branches must
    /// be made type-level impossible by restructuring — the macro is
    /// a sharp edge that silently eats real bugs.
    UnreachableMacro,
    /// `// TODO` / `// todo:` comment in any source file.
    TodoComment,
    /// `// FIXME` comment.
    FixmeComment,
    /// `// XXX` comment.
    XxxComment,
    /// `// HACK` comment.
    HackComment,
    /// `// STUB` comment.
    StubComment,
    /// `#[allow(dead_code)]` attribute. See module docs.
    AllowDeadCodeAttribute,
}

impl StubKind {
    /// Short canonical name used in finding output and CI logs.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::TodoMacro => "todo_macro",
            Self::UnimplementedMacro => "unimplemented_macro",
            Self::UnreachableMacro => "unreachable_macro",
            Self::TodoComment => "todo_comment",
            Self::FixmeComment => "fixme_comment",
            Self::XxxComment => "xxx_comment",
            Self::HackComment => "hack_comment",
            Self::StubComment => "stub_comment",
            Self::AllowDeadCodeAttribute => "allow_dead_code",
        }
    }

    /// Actionable `Fix: ...` suggestion for a given stub kind. Text is
    /// generic — callers concatenate with the file path for the
    /// finding message.
    #[must_use]
    pub const fn fix_hint(self) -> &'static str {
        match self {
            Self::TodoMacro => {
                "Fix: implement the function body instead of calling `todo!()`. \
                 If the function cannot be implemented yet, delete it and delete \
                 every caller until a real implementation lands."
            }
            Self::UnimplementedMacro => {
                "Fix: implement the function. `unimplemented!()` hides a missing \
                 contract; ship the real body or remove the call path entirely."
            }
            Self::UnreachableMacro => {
                "Fix: restructure the code so the branch is type-level impossible \
                 (match on a non-exhaustive enum, use a Result, etc). \
                 `unreachable!()` silently converts bugs into panics."
            }
            Self::TodoComment
            | Self::FixmeComment
            | Self::XxxComment
            | Self::HackComment
            | Self::StubComment => {
                "Fix: either make the change the comment describes in the same \
                 commit, or file a tracked issue and delete the comment. \
                 Undelivered intent comments rot into lies."
            }
            Self::AllowDeadCodeAttribute => {
                "Fix: either wire the unused item into a real caller or delete it. \
                 Dead code is either a bug that hasn't been caught yet or a \
                 lie about what the crate supports."
            }
        }
    }
}

impl std::fmt::Display for ZeroStubFinding {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{}: {}: {}. {}",
            self.file.display(),
            self.line,
            self.kind.name(),
            self.snippet.trim(),
            self.fix
        )
    }
}

/// Configuration knobs for the walker.
#[derive(Debug, Clone)]
pub struct ZeroStubsConfig {
    /// Roots to walk. Every `.rs` file under each root is scanned.
    pub roots: Vec<PathBuf>,
    /// Directory names (exact match) to skip entirely. Defaults
    /// include `target`, `.git`, `coordination`, `docs`, `mutants.out`.
    pub skip_dirs: Vec<String>,
    /// Files to skip by path suffix. Empty by default.
    pub allowlist: Vec<PathBuf>,
}

impl ZeroStubsConfig {
    /// Default config with a single root and the standard skip list.
    #[must_use]
    #[inline]
    pub fn with_root(root: impl Into<PathBuf>) -> Self {
        Self {
            roots: vec![root.into()],
            skip_dirs: default_skip_dirs(),
            allowlist: Vec::new(),
        }
    }
}

fn default_skip_dirs() -> Vec<String> {
    [
        "target",
        ".git",
        "coordination",
        "docs",
        "mutants.out",
        "mutants.out.old",
        "corpus",
        "corpus_staging",
    ]
    .into_iter()
    .map(String::from)
    .collect()
}

/// Scan every `.rs` file under `config.roots` and return every stub
/// finding. An empty vector means the gate passes.
///
/// # Errors
///
/// Returns a `Fix:`-prefixed error string when walkdir cannot read a
/// directory or a file cannot be read. The caller should treat an
/// `Err` as a hard gate failure — "we could not evaluate the tree"
/// is never a pass.
#[inline]
pub fn scan(config: &ZeroStubsConfig) -> Result<Vec<ZeroStubFinding>, String> {
    let mut findings = Vec::new();
    for root in &config.roots {
        scan_root(root, config, &mut findings)?;
    }
    findings.sort_by(|a, b| {
        a.file
            .cmp(&b.file)
            .then_with(|| a.line.cmp(&b.line))
            .then_with(|| a.kind.cmp(&b.kind))
    });
    Ok(findings)
}

fn scan_root(
    root: &Path,
    config: &ZeroStubsConfig,
    findings: &mut Vec<ZeroStubFinding>,
) -> Result<(), String> {
    if !root.exists() {
        return Err(format!(
            "zero-stubs gate root does not exist: {}. Fix: pass an existing directory.",
            root.display()
        ));
    }
    let walker = WalkDir::new(root).into_iter().filter_entry(|entry| {
        if entry.depth() == 0 {
            return true;
        }
        let name = entry.file_name().to_string_lossy();
        !config.skip_dirs.iter().any(|skip| skip == name.as_ref())
    });
    for entry in walker {
        let entry = entry.map_err(|error| {
            format!(
                "zero-stubs gate walker error under {}: {error}. Fix: restore read permissions.",
                root.display()
            )
        })?;
        if !entry.file_type().is_file() {
            continue;
        }
        let path = entry.path();
        if path.extension().and_then(|ext| ext.to_str()) != Some("rs") {
            continue;
        }
        if config
            .allowlist
            .iter()
            .any(|allow| path.ends_with(allow) || path == allow)
        {
            continue;
        }
        scan_file(root, path, findings)?;
    }
    Ok(())
}

fn scan_file(root: &Path, path: &Path, findings: &mut Vec<ZeroStubFinding>) -> Result<(), String> {
    let source = fs::read_to_string(path).map_err(|error| {
        format!(
            "zero-stubs gate failed to read {}: {error}. Fix: restore read permissions.",
            path.display()
        )
    })?;
    let rel = path.strip_prefix(root).unwrap_or(path).to_path_buf();

    // Line-based pass: comments and macros that are cheap to detect by
    // substring match. The parse-based pass below catches attributes
    // with higher fidelity.
    for (line_number, line) in source.lines().enumerate() {
        let line_number = line_number + 1;
        if let Some(kind) = detect_comment_marker(line) {
            findings.push(ZeroStubFinding {
                file: rel.clone(),
                line: line_number,
                kind,
                snippet: truncate(line, 160),
                fix: kind.fix_hint().to_string(),
            });
        }
        for (marker, kind) in MACRO_MARKERS {
            if contains_macro(line, marker) && !is_in_test_module_line(&rel, &source, line_number) {
                findings.push(ZeroStubFinding {
                    file: rel.clone(),
                    line: line_number,
                    kind: *kind,
                    snippet: truncate(line, 160),
                    fix: kind.fix_hint().to_string(),
                });
            }
        }
    }

    // Attribute pass: `#[allow(dead_code)]` — detected via substring
    // for speed, not via `syn` parse, because a single-pass is good
    // enough and it keeps the dependency surface small.
    for (line_number, line) in source.lines().enumerate() {
        let line_number = line_number + 1;
        let trimmed = line.trim_start();
        if trimmed.starts_with("#[allow(dead_code)]") || trimmed.starts_with("#![allow(dead_code)]")
        {
            findings.push(ZeroStubFinding {
                file: rel.clone(),
                line: line_number,
                kind: StubKind::AllowDeadCodeAttribute,
                snippet: truncate(line, 160),
                fix: StubKind::AllowDeadCodeAttribute.fix_hint().to_string(),
            });
        }
    }

    Ok(())
}

fn truncate(input: &str, max: usize) -> String {
    let trimmed = input.trim();
    if trimmed.len() <= max {
        trimmed.to_string()
    } else {
        let mut out = trimmed[..max - 1].to_string();
        out.push('');
        out
    }
}

fn detect_comment_marker(line: &str) -> Option<StubKind> {
    let trimmed = line.trim_start();
    if !trimmed.starts_with("//") {
        return None;
    }
    let body = trimmed
        .trim_start_matches('/')
        .trim_start_matches('!')
        .trim_start();
    // Case-insensitive match but anchored at the start of the comment
    // so "// TODO" in a doc string inside a code block doesn't count
    // as an English word "todo".
    let upper: String = body
        .chars()
        .take(8)
        .collect::<String>()
        .to_ascii_uppercase();
    if upper.starts_with("TODO") {
        return Some(StubKind::TodoComment);
    }
    if upper.starts_with("FIXME") {
        return Some(StubKind::FixmeComment);
    }
    if upper.starts_with("XXX") {
        return Some(StubKind::XxxComment);
    }
    if upper.starts_with("HACK") {
        return Some(StubKind::HackComment);
    }
    if upper.starts_with("STUB") {
        return Some(StubKind::StubComment);
    }
    None
}

const MACRO_MARKERS: &[(&str, StubKind)] = &[
    ("todo!", StubKind::TodoMacro),
    ("unimplemented!", StubKind::UnimplementedMacro),
    ("unreachable!", StubKind::UnreachableMacro),
];

fn contains_macro(line: &str, marker: &str) -> bool {
    let mut remaining = line;
    while let Some(idx) = remaining.find(marker) {
        // The macro must not be preceded by an identifier character —
        // `my_todo!()` or `custom_unreachable!()` are not stubs.
        let preceded_by_ident = idx > 0
            && (remaining.as_bytes()[idx - 1].is_ascii_alphanumeric()
                || remaining.as_bytes()[idx - 1] == b'_');
        // The next char after `name!` must be `(` to count as a macro
        // call. That filters out string literals like `"unreachable!"`.
        let after = &remaining[idx + marker.len()..];
        let followed_by_paren =
            after.starts_with('(') || after.starts_with(' ') && after.trim_start().starts_with('(');
        if !preceded_by_ident && followed_by_paren {
            return true;
        }
        remaining = &remaining[idx + marker.len()..];
    }
    false
}

/// Very cheap heuristic: if the file path includes a `tests/`
/// segment or the file itself is named `tests.rs`, or the line
/// is inside a `#[cfg(test)] mod tests` block, treat it as test
/// code. We deliberately don't parse the file for accuracy — the
/// test-vs-production boundary is codified in the path layout rule
/// from LAW 7 / Phase 4 structural rules.
fn is_in_test_module_line(rel: &Path, source: &str, line_number: usize) -> bool {
    if rel
        .components()
        .any(|component| component.as_os_str() == "tests")
    {
        return true;
    }
    if rel
        .file_stem()
        .and_then(|stem| stem.to_str())
        .is_some_and(|stem| stem == "tests" || stem.ends_with("_tests"))
    {
        return true;
    }
    let mut inside = false;
    let mut depth: i32 = 0;
    for (index, line) in source.lines().enumerate() {
        if index + 1 > line_number {
            break;
        }
        let trimmed = line.trim_start();
        if !inside && (trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("#[cfg(all(test"))
        {
            // Peek ahead: next `mod X {` opens a test block.
            inside = source
                .lines()
                .nth(index + 1)
                .map(|next| next.trim_start().starts_with("mod "))
                .unwrap_or(false);
            if inside {
                depth = 0;
            }
        }
        if inside {
            depth += i32::try_from(line.matches('{').count()).unwrap_or(0);
            depth -= i32::try_from(line.matches('}').count()).unwrap_or(0);
            if depth <= 0 && index + 1 > line_number {
                return true;
            }
            if depth <= 0 {
                inside = false;
            }
        }
    }
    inside
}

/// Registry entry for `zero_stubs` enforcement.
pub struct ZeroStubsEnforcer;

impl crate::enforce::EnforceGate for ZeroStubsEnforcer {
    fn id(&self) -> &'static str {
        "zero_stubs"
    }

    fn name(&self) -> &'static str {
        "zero_stubs"
    }

    fn run(&self, ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<crate::enforce::Finding> {
        let config = ZeroStubsConfig::with_root(ctx.workspace_root);
        match scan(&config) {
            Ok(findings) => crate::enforce::finding_result(
                self.id(),
                findings
                    .into_iter()
                    .map(|finding| finding.to_string())
                    .collect(),
            ),
            Err(error) => vec![crate::enforce::aggregate_finding(self.id(), vec![error])],
        }
    }
}

/// Auto-registered `zero_stubs` enforcer.
pub const REGISTERED: ZeroStubsEnforcer = ZeroStubsEnforcer;

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

    use std::io::Write;
    use tempfile::TempDir;

    fn write_file(dir: &TempDir, rel: &str, content: &str) -> PathBuf {
        let path = dir.path().join(rel);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        let mut file = std::fs::File::create(&path).unwrap();
        file.write_all(content.as_bytes()).unwrap();
        path
    }

    fn run(dir: &TempDir) -> Vec<ZeroStubFinding> {
        scan(&ZeroStubsConfig::with_root(dir.path())).expect("scan must not fail on clean tmpdir")
    }

    #[test]
    fn clean_tree_produces_no_findings() {
        let dir = TempDir::new().unwrap();
        write_file(&dir, "src/lib.rs", "pub fn answer() -> u32 { 42 }\n");
        let findings = run(&dir);
        assert!(
            findings.is_empty(),
            "a clean file must produce zero findings, got: {findings:?}"
        );
    }

    #[test]
    fn detects_todo_macro_in_production_code() {
        let dir = TempDir::new().unwrap();
        write_file(&dir, "src/lib.rs", "pub fn broken() -> u32 { todo!() }\n");
        let findings = run(&dir);
        assert_eq!(findings.len(), 1, "{findings:?}");
        assert_eq!(findings[0].kind, StubKind::TodoMacro);
        assert!(findings[0].fix.starts_with("Fix:"));
    }

    #[test]
    fn detects_unimplemented_and_unreachable() {
        let dir = TempDir::new().unwrap();
        write_file(&dir, "src/a.rs", "pub fn a() -> u32 { unimplemented!() }\n");
        write_file(
            &dir,
            "src/b.rs",
            "pub fn b() { match 0u32 { _ => unreachable!() } }\n",
        );
        let findings = run(&dir);
        let kinds: Vec<_> = findings.iter().map(|finding| finding.kind).collect();
        assert!(kinds.contains(&StubKind::UnimplementedMacro));
        assert!(kinds.contains(&StubKind::UnreachableMacro));
    }

    #[test]
    fn todo_comment_is_detected_case_insensitively() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "src/lib.rs",
            "// TODO: wire this up\n// todo: lowercase also\npub fn live() {}\n",
        );
        let findings = run(&dir);
        let kinds: Vec<_> = findings.iter().map(|finding| finding.kind).collect();
        assert!(
            kinds
                .iter()
                .filter(|kind| **kind == StubKind::TodoComment)
                .count()
                >= 2,
            "both TODO comments should fire: {findings:?}"
        );
    }

    #[test]
    fn fixme_xxx_hack_stub_comments_each_have_their_own_kind() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "src/lib.rs",
            "// FIXME one\n// XXX two\n// HACK three\n// STUB four\n",
        );
        let findings = run(&dir);
        let kinds: std::collections::BTreeSet<_> =
            findings.iter().map(|finding| finding.kind).collect();
        for expected in [
            StubKind::FixmeComment,
            StubKind::XxxComment,
            StubKind::HackComment,
            StubKind::StubComment,
        ] {
            assert!(kinds.contains(&expected), "missing kind: {expected:?}");
        }
    }

    #[test]
    fn allow_dead_code_attribute_is_flagged() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "src/lib.rs",
            "#[allow(dead_code)]\nfn hidden() -> u32 { 1 }\n",
        );
        let findings = run(&dir);
        assert!(
            findings
                .iter()
                .any(|finding| finding.kind == StubKind::AllowDeadCodeAttribute),
            "expected AllowDeadCodeAttribute finding in {findings:?}"
        );
    }

    #[test]
    fn macro_in_test_module_path_is_ignored() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "src/tests/unit/broken.rs",
            "#[test] fn t() { todo!() }\n",
        );
        let findings = run(&dir);
        assert!(
            !findings
                .iter()
                .any(|finding| finding.kind == StubKind::TodoMacro),
            "in-test todo!() should be ignored: {findings:?}"
        );
    }

    #[test]
    fn substring_match_does_not_false_positive_on_suffix_identifiers() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "src/lib.rs",
            "fn f(custom_todo: u32) -> u32 { custom_todo }\nfn g() { my_unreachable!(); }\n",
        );
        let findings = run(&dir);
        assert!(
            findings.iter().all(|finding| matches!(
                finding.kind,
                StubKind::TodoComment
                    | StubKind::FixmeComment
                    | StubKind::XxxComment
                    | StubKind::HackComment
                    | StubKind::StubComment
                    | StubKind::AllowDeadCodeAttribute
            )),
            "false-positive on identifier suffix: {findings:?}"
        );
    }

    #[test]
    fn findings_are_deterministically_sorted() {
        let dir = TempDir::new().unwrap();
        write_file(&dir, "src/z.rs", "// TODO last\n");
        write_file(&dir, "src/a.rs", "// TODO first\n");
        write_file(&dir, "src/m.rs", "// TODO middle\n");
        let findings = run(&dir);
        let paths: Vec<_> = findings
            .iter()
            .map(|finding| finding.file.to_string_lossy().to_string())
            .collect();
        let mut sorted = paths.clone();
        sorted.sort();
        assert_eq!(paths, sorted, "findings must be sorted by path: {paths:?}");
    }

    #[test]
    fn display_format_is_actionable() {
        let finding = ZeroStubFinding {
            file: PathBuf::from("src/a.rs"),
            line: 42,
            kind: StubKind::TodoMacro,
            snippet: "todo!()".to_string(),
            fix: StubKind::TodoMacro.fix_hint().to_string(),
        };
        let rendered = format!("{finding}");
        assert!(rendered.contains("src/a.rs"), "{rendered}");
        assert!(rendered.contains("42"), "{rendered}");
        assert!(rendered.contains("todo_macro"), "{rendered}");
        assert!(rendered.contains("Fix:"), "{rendered}");
    }

    #[test]
    fn skip_dirs_default_excludes_target_and_docs() {
        let dir = TempDir::new().unwrap();
        write_file(
            &dir,
            "target/poison.rs",
            "// TODO target must never appear\n",
        );
        write_file(&dir, "docs/poison.rs", "// TODO docs must never appear\n");
        write_file(&dir, "src/good.rs", "pub fn ok() {}\n");
        let findings = run(&dir);
        assert!(
            findings.is_empty(),
            "default skip dirs should exclude everything in this scenario: {findings:?}"
        );
    }

    #[test]
    fn allowlist_suppresses_specific_files() {
        let dir = TempDir::new().unwrap();
        write_file(&dir, "src/ok.rs", "pub fn ok() {}\n");
        write_file(&dir, "src/waived.rs", "// TODO tracked elsewhere\n");
        let config = ZeroStubsConfig {
            roots: vec![dir.path().to_path_buf()],
            skip_dirs: default_skip_dirs(),
            allowlist: vec![PathBuf::from("src/waived.rs")],
        };
        let findings = scan(&config).unwrap();
        assert!(
            findings.is_empty(),
            "allowlisted file must not produce findings: {findings:?}"
        );
    }

    #[test]
    fn missing_root_returns_actionable_error() {
        let config = ZeroStubsConfig::with_root("/definitely/does/not/exist/vyre-zero-stubs");
        let error = scan(&config).unwrap_err();
        assert!(
            error.starts_with("zero-stubs gate root does not exist"),
            "{error}"
        );
        assert!(error.contains("Fix:"), "{error}");
    }

    #[test]
    fn stub_kind_names_are_unique() {
        use std::collections::BTreeSet;
        let names: BTreeSet<_> = [
            StubKind::TodoMacro,
            StubKind::UnimplementedMacro,
            StubKind::UnreachableMacro,
            StubKind::TodoComment,
            StubKind::FixmeComment,
            StubKind::XxxComment,
            StubKind::HackComment,
            StubKind::StubComment,
            StubKind::AllowDeadCodeAttribute,
        ]
        .into_iter()
        .map(|kind| kind.name())
        .collect();
        assert_eq!(names.len(), 9, "{names:?}");
    }
}