alef 0.106.1

Opinionated polyglot binding generator for Rust libraries
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
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
//! The fresh-render drift comparison ("does this already-owned, already-present file still
//! match what this run would produce") -- alef#436's second half, extended by alef#508 to the
//! markerless paths whose ownership is proven by `.alef-ownership.toml`, and the home for
//! [`drifted_managed_paths`], the two-tier comparison `bin_cli::helpers::find_missing_and_frozen_
//! generated_files` calls into.
//!
//! # What this has to get right
//!
//! A fresh in-memory render is NOT what ends up on disk. `alef all` writes the render and then
//! reformats it -- `poly fmt --fix`, `cargo fmt --all`, `cargo sort -n -w`, `mix format`, a
//! `[crates.e2e.format]` override -- and only then stamps the hash. So "did this file drift"
//! can only be answered against the render PLUS whatever alef's own pass does to it at that
//! path. Getting the "whatever" wrong in either direction is a real defect:
//!
//! - Model too much formatting and the check reports drift the writer can never clear.
//! - Model too little and it certifies a stale file as fresh.
//!
//! [`crate::cli::pipeline::formatting_owner`] is the single answer to "what formats this path",
//! shared with the writer. This module consumes it; it does not keep its own copy.
//!
//! # The alef#478 defect
//!
//! This module used to hold a second, one-line, wrong copy of that answer: *`poly fmt`, always*,
//! applied to a temp sibling of the real file. In a consumer repo that manufactured **197**
//! drift findings against a tree two consecutive `alef all` runs left byte-identical -- **zero**
//! of the 197 overlapped the set `alef all` actually rewrites. Three independent causes, all
//! measured:
//!
//! 1. **poly's excludes are name-keyed, and a temp sibling escapes them.** The real
//!    `crates/<crate>-ffi/Cargo.toml` is excluded by the consumer's `**/Cargo.toml`;
//!    `.alef-verify-drift-<rand>.toml` beside it is not. The check compared disk against a
//!    taplo-reformatted rendering of a file poly never touches.
//! 2. **Trees poly's discovery prunes.** A gitignored directory, or a bare `e2e/**` glob that
//!    (per poly's own documentation) also prunes `src/test/java/io/<org>/e2e/`, where `e2e` is a
//!    package name.
//! 3. **Formatters the check modelled not at all** -- the residuals and `[crates.e2e.format]`
//!    overrides above. Elixir was the sharpest: poly is excluded from `.ex`/`.exs` on BOTH
//!    sides, so the temp copy was a no-op and the check demanded that disk equal the raw
//!    emitter output while the writer had run `mix format` over it.
//!
//! Measured against the same consumer: of those 197 paths, poly's own pass reaches **32**; the
//! other **165** it never sees. ~keep
//!
//! # Why `.rs` and `.md` are no longer symmetric (alef#458, alef#465, alef#469, alef#478)
//!
//! Both extensions once took a fast path that skipped the formatter entirely, on the theory
//! that [`crate::cli::pipeline::normalize_content`] reproduces what poly would do to them.
//! That is true for `.rs` and **false for `.md`**, and alef#458 got here by reasoning about
//! implementation symmetry instead of measuring each one.
//!
//! `.rs` keeps the fast path only when poly is absent. `normalize_content` runs a REAL `rustfmt`
//! (`format_rust_content`), so that prediction is exact when no later formatter runs. With poly
//! present, however, the writer gives poly the final word and verification must do the same:
//! consumer configuration can make poly's Rust pass differ from the in-memory normalization.
//! alef#465 still proves the no-poly case end to end with a real from-scratch `alef all`. ~keep
//!
//! `.md` loses it whenever poly is present. alef models exactly one rumdl rule: a blank-line cap
//! matched to **MD012** (`normalize_whitespace_with_policy`). The poly config alef itself
//! generates (`scaffold::languages::poly`'s `RUMDL_DISABLE`) **disables MD012** -- so the one
//! rule alef reproduces is the one rule that is switched off, and every rule that actually
//! reshapes alef's markdown is left enabled and unmodelled.
//!
//! Measured in a consumer, by disabling each rule in turn and re-running the real formatter over
//! the raw render: a cold `alef readme` left **11 of 16** generated READMEs differing from
//! committed and a cold `alef docs` **18 of 23** reference pages, and a real
//! `poly fmt --fix --fix-generated` reconciled **all 29** exactly. The responsible rules are
//! **MD032** (blanks around lists -- all 11 READMEs), **MD038** (spaces inside code spans -- 17
//! of the 18 docs pages), **MD022** (blanks around headings -- 2 pages) and **MD040** (1). Note
//! MD032 and MD022 are one emitter defect wearing two rule names: alef emits a heading with no
//! blank line after it, and which rule repairs it depends only on whether a list or prose
//! follows. **Zero** of the 29 differ by MD022 alone, so a prediction written against MD022 --
//! the first and wrong guess at this -- would have missed every README.
//!
//! The writer was a correct fixed point the whole time and this check was wrong about all 29.
//! With poly ABSENT the prediction is exact again -- nothing reformats the file after alef
//! writes it -- so the fast path is kept for that case and only that case.
//!
//! The lasting lesson is why this module no longer enumerates rules at all: the fix is not to
//! model MD032 and MD038 too, it is to stop predicting and ask the real formatter. Any list of
//! rules here would be one poly release away from being wrong again. ~keep
//!
//! # Why the temp files live beside the real file, not in a scratch directory
//!
//! poly resolves `poly.toml` (and each engine's own project markers -- `pyproject.toml`,
//! `tsconfig.json`, ...) by walking up from the target file's own directory. A scratch directory
//! elsewhere would need every one of those mirrored alongside it, which is exactly the
//! "reproduce poly's behaviour" trap this module exists to avoid. A temporary child directory
//! beside the real file preserves its exact basename while resolving the same ancestor configs.
//! Its name is deliberately not dot-prefixed: poly prunes hidden directories before formatter
//! dispatch, so a hidden staging directory makes every comparison use the raw render even when
//! the writer formatted the real file. ~keep
//!
//! Staging is now confined to paths [`crate::cli::pipeline::FormattingOwner::Poly`] actually
//! claims. `alef verify` is documented read-only, and on the consumer above the old
//! unconditional version wrote **2156** temp files into the working tree on a single run. ~keep
//!
//! # Why one batched `poly fmt --fix` invocation, not one per file
//!
//! `poly` is a subprocess; spawning one per candidate turns a verify run into hundreds of
//! process spawns. Staged files are batched by the writer context that owns them: one repo-root
//! invocation plus one per E2E language directory represented in the candidates. ~keep

use crate::cli::pipeline::{FormattingOwner, PolyCoverage, formatting_owner};
use crate::core::config::ResolvedCrateConfig;
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};

/// One file whose fast (`.rs`) prediction does not apply and whose bytes poly does shape,
/// paired with its current disk content and this run's freshly rendered content -- already
/// through [`crate::cli::commands::adopt::managed_outputs`]'s normalization/header pass, i.e.
/// the bytes `write_files` would put on disk BEFORE the post-write `poly fmt --fix` pass.
pub(super) struct RealFormatCandidate {
    pub(super) full_path: PathBuf,
    pub(super) disk_content: String,
    pub(super) rendered_content: String,
    pub(super) format_context: PathBuf,
}

/// What [`drifted_managed_paths`] was able to say about each candidate, and what it could not.
///
/// Surfaced all the way to `alef verify`'s own report (`bin_cli::core_commands::verify`) so a
/// gap is a loud, counted skip rather than a silent pass -- this repo's dominant defect shape is
/// a check that examines nothing and still reports clean (the `prove-the-check-fired` rule).
/// No skip is ever folded into `compared`: they answer different questions -- "did the check
/// run" and "what did it find" -- and a report that only stated the sum could not tell a clean,
/// fully-examined tree from one this call gave up on. ~keep
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub(crate) struct FormatDriftStats {
    pub(crate) compared: usize,
    pub(crate) skipped_missing_formatter: usize,
    /// Candidates whose temp copy could not be written at all -- a read-only checkout, a
    /// permission-denied directory. Counted apart from `skipped_missing_formatter` because the
    /// remedy is different and a report that blamed a missing `poly` for a read-only tree would
    /// send the reader after the wrong thing. ~keep
    pub(crate) skipped_staging_error: usize,
    /// Candidates whose formatter invocation failed after staging. Their raw staged bytes are
    /// not a substitute for the writer's formatted output, so they are never counted as compared. ~keep
    pub(crate) skipped_formatter_error: usize,
    /// Candidates alef reformats with a tool whose output it does not model, so no honest
    /// byte-level prediction exists: a `[crates.e2e.format]` override (opaque user shell), or a
    /// residual `cargo sort -n -w` / `mix format` pass.
    ///
    /// The alternative to counting these is to compare against the raw render anyway, which is
    /// precisely what produced alef#478's 197 false findings. Passing them silently would be
    /// the opposite defect. Counting is the only honest third option, and it is why this field
    /// exists rather than the paths simply being dropped. ~keep
    pub(crate) skipped_no_faithful_prediction: usize,
    /// Candidates settled by byte equality with the render itself, with no formatter tier
    /// involved -- the `.rs`/poly-absent-`.md` fast paths and every
    /// [`FormattingOwner::None`] path, i.e. exactly the files where alef runs nothing that
    /// could reshape them and the render therefore IS the final bytes.
    ///
    /// Reported beside `compared` rather than folded into it because the two are different
    /// grades of evidence: `compared` means a real formatter ran and the result matched, this
    /// means no formatter was needed. Folding them would hide which of the two a clean report
    /// rested on. ~keep
    pub(crate) matched_render_exactly: usize,
    /// Absolute paths whose on-disk bytes were actually compared with this run's rendered
    /// bytes, regardless of whether that comparison matched or produced a drift finding.
    /// Coverage consumes the set itself rather than deriving a count from the counters, so
    /// duplicate multi-crate emits cannot overstate how much of the managed surface was checked. ~keep
    pub(crate) content_verified_paths: HashSet<PathBuf>,
}

#[derive(Default)]
struct DriftAccumulator {
    drifted: Vec<String>,
    real_format_candidates: Vec<RealFormatCandidate>,
    unpredictable: usize,
    matched_render: usize,
    content_verified_paths: HashSet<PathBuf>,
}

struct TextDriftContext<'a> {
    base_dir: &'a Path,
    config: &'a ResolvedCrateConfig,
    coverage: &'a PolyCoverage,
    poly_available: bool,
}

impl FormatDriftStats {
    /// Fold one crate's counters into a multi-crate total.
    ///
    /// Exhaustively destructured rather than field-by-field `+=`, so adding a counter to this
    /// struct fails to compile here instead of silently reporting zero for it across every
    /// multi-crate run -- the same class of quiet under-count this whole type exists to make
    /// impossible. ~keep
    pub(crate) fn add(&mut self, other: Self) {
        let Self {
            compared,
            skipped_missing_formatter,
            skipped_staging_error,
            skipped_formatter_error,
            skipped_no_faithful_prediction,
            matched_render_exactly,
            content_verified_paths,
        } = other;
        self.compared += compared;
        self.skipped_missing_formatter += skipped_missing_formatter;
        self.skipped_staging_error += skipped_staging_error;
        self.skipped_formatter_error += skipped_formatter_error;
        self.skipped_no_faithful_prediction += skipped_no_faithful_prediction;
        self.matched_render_exactly += matched_render_exactly;
        self.content_verified_paths.extend(content_verified_paths);
    }
}

/// Compare every `candidates` entry against disk through a real `poly fmt --fix` pass over a
/// colocated temp copy of its rendered bytes -- see the module doc for why the exact basename and
/// writer working directory are preserved and why formatting is batched. Returns the drifted subset (as `full_path.display()` strings, matching every
/// other list in [`super::MissingAndFrozenFiles`]) alongside [`FormatDriftStats`].
///
/// Gates the up-front "is poly even installed" decision through the injected `is_available`
/// rather than reading PATH directly, so the skip-and-count branch is provable without
/// depending on whether the host running the suite happens to have `poly`. There is
/// The production path always runs the real formatter; the runner seam exists only so tests can
/// prove filename-based dispatch without depending on the host's configured engines. ~keep
fn real_formatter_drift_with(
    candidates: Vec<RealFormatCandidate>,
    base_dir: &Path,
    is_available: &dyn Fn(&str) -> bool,
) -> (Vec<String>, FormatDriftStats) {
    real_formatter_drift_with_runner(
        candidates,
        base_dir,
        is_available,
        &crate::cli::pipeline::poly_format_strict_including_excluded,
    )
}

fn real_formatter_drift_with_runner(
    candidates: Vec<RealFormatCandidate>,
    _base_dir: &Path,
    is_available: &dyn Fn(&str) -> bool,
    run_formatter: &dyn Fn(&[PathBuf], &Path) -> anyhow::Result<()>,
) -> (Vec<String>, FormatDriftStats) {
    if candidates.is_empty() {
        return (Vec::new(), FormatDriftStats::default());
    }
    if !is_available("poly") {
        return (
            Vec::new(),
            FormatDriftStats {
                skipped_missing_formatter: candidates.len(),
                ..FormatDriftStats::default()
            },
        );
    }

    let temp_files: Vec<Option<StagedFormatFile>> = candidates
        .iter()
        .map(
            |candidate| match write_sibling_temp_file(&candidate.full_path, &candidate.rendered_content) {
                Ok(temp_file) => Some(temp_file),
                Err(error) => {
                    tracing::debug!(
                        path = %candidate.full_path.display(),
                        "could not stage a drift-check temp copy ({error}); skipping this file's \
                         formatted-output comparison"
                    );
                    None
                }
            },
        )
        .collect();

    let mut paths_by_context: BTreeMap<&Path, Vec<PathBuf>> = BTreeMap::new();
    for (candidate, temp_file) in candidates.iter().zip(&temp_files) {
        if let Some(temp_file) = temp_file {
            paths_by_context
                .entry(&candidate.format_context)
                .or_default()
                .push(temp_file.path().to_path_buf());
        }
    }
    let failed_contexts = run_format_candidates(paths_by_context, run_formatter);
    compare_formatted_candidates(&candidates, &temp_files, &failed_contexts)
}

/// Run each writer-context formatter batch and return the contexts whose bytes cannot be trusted
/// because the formatter failed. ~keep
fn run_format_candidates(
    paths_by_context: BTreeMap<&Path, Vec<PathBuf>>,
    run_formatter: &dyn Fn(&[PathBuf], &Path) -> anyhow::Result<()>,
) -> HashSet<PathBuf> {
    let mut failed = HashSet::new();
    for (context, paths) in paths_by_context {
        if let Err(error) = run_formatter(&paths, context) {
            tracing::warn!(
                context = %context.display(),
                "poly fmt over the drift-check temp copies failed (non-fatal): {error:#}"
            );
            failed.insert(context.to_path_buf());
        }
    }
    failed
}

/// Compare only staged files whose formatter completed and whose formatted bytes remain readable. ~keep
fn compare_formatted_candidates(
    candidates: &[RealFormatCandidate],
    temp_files: &[Option<StagedFormatFile>],
    failed_contexts: &HashSet<PathBuf>,
) -> (Vec<String>, FormatDriftStats) {
    let mut drifted = Vec::new();
    let mut compared = 0usize;
    let mut staging_errors = 0usize;
    let mut formatter_errors = 0usize;
    let mut content_verified_paths = HashSet::new();
    for (candidate, temp_file) in candidates.iter().zip(temp_files.iter()) {
        let Some(temp_file) = temp_file else {
            staging_errors += 1;
            continue;
        };
        if failed_contexts.contains(&candidate.format_context) {
            formatter_errors += 1;
            continue;
        }
        let Ok(formatted) = std::fs::read_to_string(temp_file.path()) else {
            staging_errors += 1;
            continue;
        };
        compared += 1;
        content_verified_paths.insert(candidate.full_path.clone());
        if !crate::cli::pipeline::matches_alef_output(&candidate.full_path, &candidate.disk_content, &formatted) {
            drifted.push(candidate.full_path.display().to_string());
        }
    }

    (
        drifted,
        FormatDriftStats {
            compared,
            skipped_staging_error: staging_errors,
            skipped_formatter_error: formatter_errors,
            content_verified_paths,
            ..FormatDriftStats::default()
        },
    )
}

/// Write `content` under its exact basename in a securely-named temp directory beside
/// `real_path` -- see the module doc for why this must stay colocated rather than use scratch.
///
/// `TempDir` is deliberate: it preserves the exact basename formatter dispatch needs and deletes
/// itself on drop, including after a panic or early return. ~keep
struct StagedFormatFile {
    _directory: tempfile::TempDir,
    path: PathBuf,
}

impl StagedFormatFile {
    fn path(&self) -> &Path {
        &self.path
    }
}

fn write_sibling_temp_file(real_path: &Path, content: &str) -> std::io::Result<StagedFormatFile> {
    let parent = real_path.parent().unwrap_or_else(|| Path::new("."));
    let file_name = real_path
        .file_name()
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "formatter candidate has no file name"))?;
    // A dot-prefixed temp directory is invisible to poly's discovery, even when its contained
    // file is passed explicitly. Security comes from tempfile's random name and permissions,
    // not from hiding the directory. ~keep
    let directory = tempfile::Builder::new()
        .prefix("alef-verify-drift-")
        .tempdir_in(parent)?;
    let path = directory.path().join(file_name);
    std::fs::write(&path, content)?;
    Ok(StagedFormatFile {
        _directory: directory,
        path,
    })
}

/// Absolute paths of every file in `files` that already exists on disk, is owned through either
/// alef's provenance marker or the committed ownership record, and yet no longer matches the
/// bytes this run's fresh render would produce once alef's own formatting pass has had its say.
///
/// THE GAP this closes (alef#436): a public type gaining a field left `alef docs`' rendered API
/// reference pages stale while `alef verify` stayed green, because every other check was
/// structurally blind to it:
///
/// - [`super::stale_among`]'s per-file `alef:hash:` check recomputes a hash of the file's OWN
///   current bytes and compares it to the hash embedded in those same bytes — it catches a
///   hand-edit, never a file that is internally self-consistent but stale relative to a fresh
///   render.
/// - The crate-scoped `inputs_hash` record (`cache::generation_record`,
///   `bin_cli::core_commands::verify::run`) is written only by `alef generate`/`alef all`, never
///   by `alef docs` — so a docs-only regeneration gap never re-stamps it, but neither does
///   leaving docs untouched ever unstamp it.
/// - `missing_managed_paths`/`frozen_managed_paths` only ever ask "does this path exist" and
///   "does it carry a marker" — never "do its bytes match".
///
/// Three tiers, dispatched by [`formatting_owner`] (see the module doc for the alef#478 defect
/// that having a second, private answer to the same question produced):
///
/// - **Nothing reformats it** ([`FormattingOwner::None`]) — the render IS the final bytes, so
///   compare them directly. This is the large majority of a real tree, and the tier the old
///   code was missing entirely: it staged a temp copy and ran poly over paths poly never sees.
/// - **poly reformats it** ([`FormattingOwner::Poly`]) — run the REAL `poly fmt --fix` over the
///   rendered bytes under the same basename in a colocated temp directory and compare the result. Exact rather than
///   approximate, and immune to poly gaining or losing an engine.
/// - **alef reformats it with something it cannot model** ([`FormattingOwner::Residual`],
///   [`FormattingOwner::E2eOverride`]) — counted into
///   [`FormatDriftStats::skipped_no_faithful_prediction`] and reported, never silently passed.
///
/// `.rs` short-circuits ahead of all three, and `.md` does too when poly is absent -- see
/// [`render_predicts_final_bytes`] and the module doc for why those two are not the same case.
///
/// Deliberately excludes every create-once seed outside the E2E roots alef rewrites on each run:
/// after first creation the ordinary writer preserves its consumer-owned bytes, so a current
/// render is not an authoritative freshness prediction.
/// Also excludes every file `frozen_managed_paths` would already report: an unmarked
/// file is that check's condition, not this one's, and reporting the same withheld write under
/// two headings would describe it as two findings with two remedies. Base64 binary outputs are
/// decoded and compared as raw bytes instead of routed through the text formatter path.
///
/// Reuses [`crate::cli::commands::adopt::managed_outputs`] and
/// [`crate::cli::pipeline::matches_alef_output`] — the exact pairing `frozen_managed_paths`
/// already uses — rather than a bespoke comparison, because of the caveat that pairing already
/// solves: a self-marking backend (pyo3's `lib.rs`, `docs::render`'s HTML-commented pages) or a
/// `generated_header: true` file both legitimately differ from a naive render by exactly the
/// provenance header/hash line, and `matches_alef_output` is the one place that discounts it. ~keep
pub(super) fn drifted_managed_paths(
    files: &[crate::core::backend::GeneratedFile],
    base_dir: &Path,
    config: &ResolvedCrateConfig,
) -> (Vec<String>, FormatDriftStats) {
    drifted_managed_paths_with(files, base_dir, config, &crate::cli::pipeline::is_tool_available)
}

fn record_direct_comparison(full_path: &Path, existing: &str, rendered: &str, result: &mut DriftAccumulator) {
    result.content_verified_paths.insert(full_path.to_path_buf());
    if crate::cli::pipeline::matches_alef_output(full_path, existing, rendered) {
        result.matched_render += 1;
    } else {
        result.drifted.push(full_path.display().to_string());
    }
}

/// An unchanged render never reaches the writer's formatter: the write guard leaves the already
/// hash-stamped file in place, and poly preserves it without `--fix-generated`. Formatting an
/// unstamped staging copy would model a write that the real pipeline does not perform. ~keep
fn writer_skips_unchanged_stamped_file(full_path: &Path, existing: &str, rendered: &str) -> bool {
    crate::core::hash::content_has_alef_marker(existing)
        && crate::cli::pipeline::matches_alef_output(full_path, existing, rendered)
}

fn is_under_rewritten_output_root(path: &Path, config: &ResolvedCrateConfig, base_dir: &Path) -> bool {
    config.e2e.as_ref().is_some_and(|e2e| {
        [base_dir.join(&e2e.output), base_dir.join(&e2e.registry.output)]
            .iter()
            .any(|root| path.starts_with(root))
    })
}

fn is_authoritative_drift_candidate(
    file: &crate::core::backend::GeneratedFile,
    base_dir: &Path,
    config: &ResolvedCrateConfig,
    declared: &crate::core::config::UserOwnedPaths,
) -> Option<PathBuf> {
    let full_path = base_dir.join(&file.path);
    if declared.matches(base_dir, &full_path) || !full_path.exists() {
        return None;
    }
    let has_marker = std::fs::read_to_string(&full_path)
        .ok()
        .is_some_and(|content| crate::core::hash::content_has_alef_marker(&content));
    let recorded = !has_marker
        && crate::cli::pipeline::marker_comment_style(&full_path).is_none()
        && crate::cli::cache::is_committed_scaffold_owned_path(base_dir, &full_path);
    if !has_marker && !recorded {
        return None;
    }
    if is_create_once_for_drift(file) && !is_under_rewritten_output_root(&full_path, config, base_dir) {
        return None;
    }
    Some(full_path)
}

fn prepared_text_drift_input(
    file: &crate::core::backend::GeneratedFile,
    full_path: PathBuf,
    base_dir: &Path,
) -> anyhow::Result<(PathBuf, String, String)> {
    let existing = std::fs::read_to_string(&full_path)
        .map_err(|error| anyhow::anyhow!("failed to read owned text output {}: {error}", full_path.display()))?;
    let rendered = managed_output_for_drift(file, &existing, base_dir)
        .into_iter()
        .next()
        .ok_or_else(|| anyhow::anyhow!("fresh render omitted owned text output {}", full_path.display()))?;
    Ok((full_path, existing, rendered.content))
}

/// Compare a base64-carried generated artifact with its raw on-disk bytes. Decode failures are
/// findings but not successful comparisons, so coverage never promotes them. ~keep
fn compare_binary_drift(file: &crate::core::backend::GeneratedFile, full_path: &Path, result: &mut DriftAccumulator) {
    let generated = crate::cli::pipeline::decode_base64_binary(&file.path, &file.content);
    let existing = std::fs::read(full_path);
    match (generated, existing) {
        (Ok(generated), Ok(existing)) => {
            result.content_verified_paths.insert(full_path.to_path_buf());
            if generated == existing {
                result.matched_render += 1;
            } else {
                result.drifted.push(full_path.display().to_string());
            }
        }
        (Err(error), _) => {
            tracing::warn!(
                path = %full_path.display(),
                "generated binary output could not be decoded during drift verification: {error:#}"
            );
            result.drifted.push(full_path.display().to_string());
        }
        (_, Err(error)) => {
            tracing::warn!(
                path = %full_path.display(),
                "owned binary output could not be read during drift verification: {error}"
            );
            result.drifted.push(full_path.display().to_string());
        }
    }
}

/// Route one authoritative text output through the same normalization and formatter ownership
/// policy as the writer, recording either a direct comparison or a real-formatter candidate. ~keep
fn compare_text_drift(
    file: &crate::core::backend::GeneratedFile,
    full_path: PathBuf,
    context: &TextDriftContext<'_>,
    result: &mut DriftAccumulator,
) {
    let (full_path, existing, rendered) = match prepared_text_drift_input(file, full_path, context.base_dir) {
        Ok(prepared) => prepared,
        Err(error) => {
            tracing::warn!("owned text output could not be compared during drift verification: {error:#}");
            result
                .drifted
                .push(context.base_dir.join(&file.path).display().to_string());
            return;
        }
    };
    if writer_skips_unchanged_stamped_file(&full_path, &existing, &rendered) {
        result.matched_render += 1;
        result.content_verified_paths.insert(full_path);
        return;
    }
    if render_predicts_final_bytes(&full_path, context.poly_available) {
        record_direct_comparison(&full_path, &existing, &rendered, result);
        return;
    }
    match formatting_owner(&full_path, context.config, context.base_dir, context.coverage) {
        FormattingOwner::None => record_direct_comparison(&full_path, &existing, &rendered, result),
        FormattingOwner::Poly => result.real_format_candidates.push(RealFormatCandidate {
            full_path,
            disk_content: existing,
            rendered_content: rendered,
            format_context: context
                .coverage
                .format_context(&context.base_dir.join(&file.path))
                .unwrap_or(context.base_dir)
                .to_path_buf(),
        }),
        FormattingOwner::Residual(command) => {
            tracing::debug!(
                path = %full_path.display(),
                "no faithful prediction: alef reformats this path with `{command}`, whose output it does not model"
            );
            result.unpredictable += 1;
        }
        FormattingOwner::E2eOverride(command) => {
            tracing::debug!(
                path = %full_path.display(),
                "no faithful prediction: a [crates.e2e.format] override (`{command}`) replaces the poly pass here"
            );
            result.unpredictable += 1;
        }
    }
}

/// Testable seam for [`drifted_managed_paths`]: resolves poly's availability through
/// `is_available` rather than PATH, so both the counted-skip branch and the `.md` fast path's
/// poly-absent case are provable on a host that does have poly installed. ~keep
fn drifted_managed_paths_with(
    files: &[crate::core::backend::GeneratedFile],
    base_dir: &Path,
    config: &ResolvedCrateConfig,
    is_available: &dyn Fn(&str) -> bool,
) -> (Vec<String>, FormatDriftStats) {
    let poly_available = is_available("poly");
    let coverage = PolyCoverage::probe(&poly_probe_roots(config, base_dir), base_dir);
    let declared = crate::cli::pipeline::declared_user_owned(base_dir)
        .unwrap_or_else(|_| crate::core::config::UserOwnedPaths::none());
    let text_context = TextDriftContext {
        base_dir,
        config,
        coverage: &coverage,
        poly_available,
    };
    let mut result = DriftAccumulator::default();
    for file in files {
        let Some(full_path) = is_authoritative_drift_candidate(file, base_dir, config, &declared) else {
            continue;
        };
        if crate::cli::pipeline::is_base64_binary_output(&file.path) {
            compare_binary_drift(file, &full_path, &mut result);
            continue;
        }
        compare_text_drift(file, full_path, &text_context, &mut result);
    }
    let (real_drifted, mut stats) = real_formatter_drift_with(result.real_format_candidates, base_dir, is_available);
    result.drifted.extend(real_drifted);
    stats.content_verified_paths.extend(result.content_verified_paths);
    stats.skipped_no_faithful_prediction = result.unpredictable;
    stats.matched_render_exactly = result.matched_render;
    (result.drifted, stats)
}

/// Whether the normal writer preserves an existing scaffold rather than treating this render as
/// authoritative. `.gitattributes` is the one self-marked create-once scaffold: its literal
/// `Generated by alef scaffold` line makes the generic classifier see a marker even though its
/// emitter sets `generated_header = false` and the normal scaffold stage uses overwrite=false.
/// Keep the exception exact so self-marked derived output remains freshness-checked. ~keep
fn is_create_once_for_drift(file: &crate::core::backend::GeneratedFile) -> bool {
    crate::cli::commands::adopt::is_create_once_seed(file)
        || (!file.generated_header && file.path == Path::new(".gitattributes"))
}

/// Prepare the bytes the writer would compare for drift, including the TOML merge that preserves
/// consumer-owned entries and comments in managed manifests such as `poly.toml`, and the
/// disk-aware marker preservation used for formerly generated create-once manifests. ~keep
fn managed_output_for_drift(
    file: &crate::core::backend::GeneratedFile,
    existing: &str,
    base_dir: &Path,
) -> Vec<crate::cli::commands::adopt::ManagedOutput> {
    let mut prepared = file.clone();
    if crate::core::hash::content_has_alef_marker(existing) {
        prepared.generated_header = true;
    }
    if file.generated_header && file.path == Path::new("poly.toml") {
        prepared.content = match crate::cli::pipeline::generate::merge_managed_toml_preview(
            existing,
            &file.content,
            base_dir,
            &file.path,
        ) {
            // The merge keeps the on-disk header, hash line included, and `poly fmt` skips a
            // hash-stamped file. Left in, the staged copy would never be formatted, so the
            // check would compare disk against the raw merge -- whose generator-spelled arrays
            // (`[[hooks.sources]]`'s multi-line `hooks`) differ from the poly-canonical bytes
            // the writer's own format pass leaves on disk, and `alef all` followed by
            // `alef verify` could never agree. The writer unstamps before formatting
            // (`unstamp_before_formatting`); do the same here. ~keep
            Ok(content) => crate::core::hash::strip_hash_line(&content),
            Err(error) => {
                tracing::warn!(
                    path = %file.path.display(),
                    "could not preview managed TOML merge while checking drift: {error:#}"
                );
                file.content.clone()
            }
        };
    }
    crate::cli::commands::adopt::managed_outputs(std::slice::from_ref(&prepared), base_dir)
}

/// The roots to ask poly about, matching the roots alef's own formatting pass hands it.
///
/// `base_dir` is what [`crate::cli::pipeline::converge_full_regen_formatting`] formats on a full
/// regen. Each existing e2e language directory is added because `e2e::format::format_language`
/// names each `<output>/<lang>` directory DIRECTLY and runs from there, and poly's discovery answers differently for a
/// directory it was handed than for the same directory reached by walking down from a parent --
/// a gitignored tree is the ordinary case. Asking about both is what keeps the reader's notion
/// of coverage equal to the writer's rather than merely similar. Non-existent roots are dropped:
/// poly errors on a path that is not there, which would take the whole probe down with it. ~keep
fn poly_probe_roots(config: &ResolvedCrateConfig, base_dir: &Path) -> Vec<PathBuf> {
    let mut roots = vec![base_dir.to_path_buf()];
    if let Some(e2e) = &config.e2e {
        for output in [e2e.output.as_str(), e2e.registry.output.as_str()] {
            let output_root = base_dir.join(output);
            let Ok(entries) = std::fs::read_dir(output_root) else {
                continue;
            };
            let mut language_roots: Vec<PathBuf> = entries
                .filter_map(Result::ok)
                .map(|entry| entry.path())
                .filter(|path| path.is_dir())
                .collect();
            language_roots.sort();
            for path in language_roots {
                if !roots.contains(&path) {
                    roots.push(path);
                }
            }
        }
    }
    roots
}

/// Whether [`crate::cli::pipeline::normalize_content`]'s in-memory normalization is a faithful
/// PREDICTION of the bytes `alef generate`/`alef all` actually leave on disk at `path`.
///
/// `.rs`: only when poly is absent. `normalize_content` runs a real `rustfmt`, but poly owns the
/// final bytes when installed and may apply different project configuration.
///
/// `.md`: only when poly is ABSENT. With poly present, rumdl reformats the page afterwards and
/// alef models only its MD012 rule -- which the poly config alef itself generates has DISABLED,
/// while leaving MD022 enabled. See the module doc for the measured alef#478 evidence, and for
/// why the two extensions must keep being reasoned about separately rather than kept symmetric.
///
/// Everything else goes to [`formatting_owner`], which answers from the shared policy rather
/// than from an extension list. ~keep
fn render_predicts_final_bytes(path: &Path, poly_available: bool) -> bool {
    match path.extension().and_then(|extension| extension.to_str()) {
        Some("rs") => !poly_available,
        Some("md") => !poly_available,
        _ => false,
    }
}

/// Print `alef verify`'s formatted-output drift coverage line.
///
/// Printed unconditionally, on every run including a clean one, for the same reason
/// `VerifyCoverage` reports scan coverage unconditionally: a check that examined nothing must
/// never render identically to one that found a clean tree (the `prove-the-check-fired` rule).
/// Every skip is named with its own remedy, because they differ: install `poly`, make the
/// checkout writable, or accept that alef cannot speak for a path it reformats with a tool it
/// does not model. ~keep
pub(crate) fn report_format_drift_coverage(stats: FormatDriftStats) {
    let mut notes: Vec<String> = Vec::new();
    if stats.skipped_missing_formatter > 0 {
        tracing::warn!(
            "poly is not installed; {} generated file(s) could not be checked for formatted-output \
             drift (install poly to close this gap)",
            stats.skipped_missing_formatter
        );
        notes.push(format!(
            "{} SKIPPED because poly is not installed (install poly to close this gap -- see poly-lint-format)",
            stats.skipped_missing_formatter
        ));
    }
    if stats.skipped_staging_error > 0 {
        tracing::warn!(
            "{} generated file(s) could not be checked for formatted-output drift: their temp copy \
             could not be written (read-only checkout?)",
            stats.skipped_staging_error
        );
        notes.push(format!(
            "{} SKIPPED because a temp copy could not be written beside them",
            stats.skipped_staging_error
        ));
    }
    if stats.skipped_formatter_error > 0 {
        notes.push(format!(
            "{} SKIPPED because the formatter invocation failed",
            stats.skipped_formatter_error
        ));
    }
    if stats.skipped_no_faithful_prediction > 0 {
        notes.push(format!(
            "{} NOT CHECKED because alef reformats them with a tool whose output it does not model \
             (a [crates.e2e.format] override, `cargo sort`, or `mix format`) -- no honest byte-level \
             prediction exists for these",
            stats.skipped_no_faithful_prediction
        ));
    }
    let detail = if notes.is_empty() {
        "all examined.".to_string()
    } else {
        format!("{}.", notes.join("; "))
    };
    crate::bin_cli::output::line(format!(
        "Formatted-output drift check: {} file(s) compared via a real `poly fmt` pass, {} matched the \
         render with no formatter involved, {detail}",
        stats.compared, stats.matched_render_exactly
    ));
}

#[cfg(test)]
mod tests;