Skip to main content

xtask/
greenfield_status.rs

1//! `cargo xtask greenfield-status` — the probe harness (track doc §8, proposal #999).
2//!
3//! Thirteen probes measuring the tree against `design/bynk-greenfield-compiler.md`:
4//! the twelve in track doc §8, plus `emit_abi_shapes` (ADR 0310's probe, #999 Decision
5//! E — this slice measures the emit-ABI enumeration guard but does not wire it; wiring
6//! is packaging-track work).
7//!
8//! **Nine are zero/closure probes**, committed and diffed: `workspace_lints`,
9//! `fs_below_driver`, `options_sources`, `hoist_sinks`, `span_keyed_maps`,
10//! `emit_diagnostics`, `ide_emit_edge`, `ast_importers`, `emit_abi_shapes`. A
11//! disagreement between a fresh run and the committed table fails
12//! `greenfield_status_table_is_current` (`xtask/tests/greenfield_status.rs`), which
13//! rides both the `test` job (`cargo test --workspace`, any Rust-touching PR) and the
14//! `drift` job's existing `cargo test -p xtask` (pending/decisions-only PRs) — no new
15//! CI wiring (#999 Decision D, which also explains why a `drift`-job *step* would have
16//! been silently skipped on the PRs that move these probes most).
17//!
18//! **Four are count/ratio trend probes**, recomputed and printed but never diffed:
19//! `wildcard_arms`, `keep_in_sync`, `test_density`, `fixture_kinds`. These move on
20//! nearly any ordinary Rust PR (§8 calls two of them "trends, not gates"); hard-gating
21//! them would make the committed table churn, and conflict, on routine work.
22//!
23//! `Closes-Rule:` rule-id provenance (#999 Decision B) is deferred to a follow-on
24//! slice — the committed table below carries no rule-citation column yet.
25
26use std::collections::BTreeSet;
27use std::fmt::Write as _;
28use std::path::{Path, PathBuf};
29use std::process::Command;
30
31/// One probe's result. `gated` probes are diffed against the committed table by
32/// [`crate::greenfield_status::gated_disagreements`]; the rest are reported only.
33pub struct Probe {
34    pub name: &'static str,
35    pub gated: bool,
36    pub reads: String,
37}
38
39pub struct Report {
40    pub probes: Vec<Probe>,
41}
42
43impl Report {
44    pub fn get(&self, name: &str) -> &str {
45        self.probes
46            .iter()
47            .find(|p| p.name == name)
48            .map(|p| p.reads.as_str())
49            .unwrap_or_else(|| panic!("no probe named {name:?}"))
50    }
51}
52
53/// Run every probe against the tree rooted at `root` (the repo root). Used by the CLI's
54/// full report; the gating test uses the nine gated probes alone
55/// ([`gated_disagreements`]) so it never pays for a workspace-wide clippy pass
56/// (`wildcard_arms`) just to check the probes that are actually diffed.
57pub fn run(root: &Path) -> Report {
58    let mut probes = run_gated(root);
59    probes.extend(run_trend(root));
60    Report { probes }
61}
62
63/// The nine gated (zero/closure) probes only — what [`gated_disagreements`] diffs.
64fn run_gated(root: &Path) -> Vec<Probe> {
65    vec![
66        workspace_lints(root),
67        fs_below_driver(root),
68        options_sources(root),
69        hoist_sinks(root),
70        span_keyed_maps(root),
71        emit_diagnostics(root),
72        ide_emit_edge(root),
73        ast_importers(root),
74        emit_abi_shapes(root),
75    ]
76}
77
78/// The four reported-only trend probes — never diffed, and notably including the one
79/// (`wildcard_arms`) that shells out to a full `cargo clippy --workspace` pass, which
80/// the gating test must not pay for on every run.
81fn run_trend(root: &Path) -> Vec<Probe> {
82    vec![
83        wildcard_arms(root),
84        keep_in_sync(root),
85        test_density(root),
86        fixture_kinds(root),
87    ]
88}
89
90/// `design/greenfield-status.md` — the committed table this probe set regenerates.
91pub fn table_path(root: &Path) -> PathBuf {
92    root.join("design/greenfield-status.md")
93}
94
95// --- Filesystem helpers --------------------------------------------------
96
97/// Every `.rs` file under `dir`, recursively, as `(path, contents)`. Unreadable files
98/// (permissions, non-UTF-8) are skipped rather than failing the whole walk — this is a
99/// measurement tool, not a build step.
100fn rust_files(dir: &Path) -> Vec<(PathBuf, String)> {
101    let mut out = Vec::new();
102    walk(dir, &mut out);
103    out
104}
105
106fn walk(dir: &Path, out: &mut Vec<(PathBuf, String)>) {
107    let Ok(entries) = std::fs::read_dir(dir) else {
108        return;
109    };
110    let mut entries: Vec<_> = entries.flatten().collect();
111    entries.sort_by_key(|e| e.file_name());
112    for entry in entries {
113        let path = entry.path();
114        if path.is_dir() {
115            walk(&path, out);
116        } else if path.extension().is_some_and(|e| e == "rs")
117            && let Ok(contents) = std::fs::read_to_string(&path)
118        {
119            out.push((path, contents));
120        }
121    }
122}
123
124/// The inner text of every **standalone** `"bynk.<ident>"` string literal (the
125/// `bynk.*` convention used for diagnostic codes and commons/namespace paths alike).
126///
127/// Standalone, not merely prefix-matching: the identifier run must be immediately
128/// followed by the closing quote, matching the naive `rg -o '"bynk\.[a-zA-Z0-9_.]*"'`
129/// this probe is deliberately more careful than (#999 Decision A). Without that
130/// requirement this would also match the *start* of an unrelated, longer message that
131/// merely happens to begin with "bynk." — e.g. a panic string
132/// `"bynk.map itself uses bynk.list, so list must be injected too: {paths:?}"` is prose
133/// beginning with a namespace-shaped word, not a `"bynk.map"` code literal, and a
134/// dev-only compile-time error message split across lines with a `\`-continuation
135/// (`"bynk.emit.unresolved_cross_context_signature: no signature for \` ...) is one
136/// string, not a diagnostic-code literal, even though its first segment matches the
137/// identifier charset. Both were found — and wrongly counted — by an earlier,
138/// less careful version of this scan; the fix is requiring the closing quote.
139fn bynk_dotted_literals(src: &str) -> Vec<&str> {
140    let mut out = Vec::new();
141    let bytes = src.as_bytes();
142    let mut i = 0;
143    while let Some(rel) = src[i..].find("\"bynk.") {
144        let start = i + rel + 1; // skip the opening quote
145        let mut end = start;
146        while end < bytes.len()
147            && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'.')
148        {
149            end += 1;
150        }
151        if end < bytes.len() && bytes[end] == b'"' {
152            out.push(&src[start..end]);
153        }
154        i = end.max(start + 1);
155    }
156    out
157}
158
159/// True if `line`, trimmed, is a `//` or `///` or `//!` line comment. Doesn't attempt
160/// block comments (`/* */`) — none of this codebase's `bynk.*`/dead-identifier
161/// mentions live in one.
162fn is_line_comment(line: &str) -> bool {
163    line.trim_start().starts_with("//")
164}
165
166// --- Gated probe 1: workspace_lints --------------------------------------
167
168/// R2.12. `[workspace.lints]` presence and `clippy::wildcard_enum_match_arm`'s level in
169/// the root `Cargo.toml`. A boolean-shaped probe (not a count) — gated because it only
170/// ever changes once, when T0.3 lands it.
171fn workspace_lints(root: &Path) -> Probe {
172    let cargo_toml = std::fs::read_to_string(root.join("Cargo.toml")).unwrap_or_default();
173    let has_section = cargo_toml
174        .lines()
175        .any(|l| l.trim() == "[workspace.lints.clippy]" || l.trim() == "[workspace.lints]");
176    let level = cargo_toml
177        .lines()
178        .find(|l| l.contains("wildcard_enum_match_arm"))
179        .map(|l| l.trim().to_string());
180    let reads = match (has_section, level) {
181        (true, Some(l)) => format!("present — {l}"),
182        (true, None) => "present, wildcard_enum_match_arm not set".to_string(),
183        (false, _) => "absent".to_string(),
184    };
185    Probe {
186        name: "workspace_lints",
187        gated: true,
188        reads,
189    }
190}
191
192// --- Gated probe 2: fs_below_driver --------------------------------------
193
194/// R2.3. Files under `bynk-emit/src`, `bynk-ide/src`, `bynk-fmt/src` (the crates below
195/// the `bynk` driver, which owns disk I/O) that touch `std::fs` in **production** code.
196///
197/// Excludes usage inside a trailing `#[cfg(test)] mod tests { ... }` block — the
198/// convention every file in this codebase uses, always the last item in the file. A
199/// line is production-scope unless it falls at or after the line following a
200/// `#[cfg(test)]` attribute whose very next non-empty line opens a `mod ... {` block
201/// (as opposed to a `mod name;` external-file declaration, which is not a scope at
202/// all). This mirrors the comment-exclusion discipline elsewhere in this probe set:
203/// tests writing fixtures to a tempdir are not "the driver's job" bypassed, and
204/// counting them would report a rule open that the production code has already closed.
205///
206/// A file counts if its own text names `std::fs` ([`has_production_std_fs`]), **or** if
207/// a bare `fs::`-style call site in it resolves to `std::fs` through its imports
208/// ([`production_std_fs_files`]) — a module-level `use std::fs;` in a parent module is
209/// visible to a child through `use super::*;` (module privacy is ancestor-scoped), so
210/// `bynk-emit/src/project/discovery.rs` reads and walks the filesystem while never
211/// spelling `std::fs` itself. The literal text scan alone missed exactly that file,
212/// so a probe reading `bynk-emit=0` would have asserted R2.3 closed on a false
213/// premise (#1013).
214///
215/// #1104 (a content-ownership (#1086) probe-precision follow-on): a flagged *count*
216/// alone can't tell a residual R2.3 violation from a documented, permanent exception —
217/// `bynk-emit`'s 3 have read that way since the track's retirement (`design/archive/
218/// retired-tracks.md`'s closing summary), each named in [`NAMED_FS_EXCEPTIONS`]. So
219/// each flagged file is additionally classified as a **named floor** file — every
220/// production-scope touch it has is either inside one of those named functions, or is
221/// a bare import declaration (no fn encloses it — [`enclosing_fn`] returns `None`) that
222/// performs no I/O of its own, existing only so a *descendant* module's bare `fs::`
223/// call can resolve (exactly `project.rs`'s `use std::fs;`, which `discovery.rs` and
224/// `paths.rs` glob-import via `use super::*;`) — or a **residual** file: any other file
225/// touching `std::fs` in production scope, which still reads as a real R2.3 violation
226/// ([`file_is_named_fs_floor`]).
227fn fs_below_driver(root: &Path) -> Probe {
228    let crates = ["bynk-emit", "bynk-ide", "bynk-fmt"];
229    let mut per_crate = Vec::new();
230    let mut total = 0usize;
231    let mut total_floor = 0usize;
232    for krate in crates {
233        let dir = root.join(krate).join("src");
234        let files: Vec<(PathBuf, String)> = rust_files(&dir)
235            .into_iter()
236            .map(|(path, contents)| {
237                let rel = path.strip_prefix(&dir).unwrap_or(&path).to_path_buf();
238                (rel, contents)
239            })
240            .collect();
241        let flagged = production_std_fs_files(&files);
242        let count = flagged.len();
243        total += count;
244        let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
245        let parents: Vec<Option<usize>> = files
246            .iter()
247            .map(|(p, _)| module_parent(p, &files))
248            .collect();
249        let floor = flagged
250            .iter()
251            .filter(|&&i| file_is_named_fs_floor(krate, &files, &facts, &parents, i))
252            .count();
253        total_floor += floor;
254        let residual = count - floor;
255        per_crate.push(if floor > 0 {
256            format!("{krate}={count} ({floor} named floor, {residual} residual)")
257        } else {
258            format!("{krate}={count}")
259        });
260    }
261    Probe {
262        name: "fs_below_driver",
263        gated: true,
264        reads: format!(
265            "{total} files ({}) — {total_floor} named floor, {} residual total",
266            per_crate.join(", "),
267            total - total_floor
268        ),
269    }
270}
271
272/// #1104: the specific, permanently-carved-out production functions whose
273/// `std::fs` touch is a *named* exception, not evidence of unfinished R2.3
274/// migration — settled in `design/tracks/content-ownership.md` §3.2 (retired) and
275/// its closing summary in `design/archive/retired-tracks.md`. `(crate, file path
276/// relative to that crate's `src/`, enclosing production fn name)`. A future
277/// carve-out decided the same deliberate way joins this list; anything touching
278/// `std::fs` in production scope that isn't listed here reads as a residual R2.3
279/// violation, per [`file_is_named_fs_floor`].
280const NAMED_FS_EXCEPTIONS: &[(&str, &str, &str)] = &[
281    // The bare enumeration walk — no content read, no overlay parameter at all.
282    ("bynk-emit", "project/discovery.rs", "discover_bynk_files"),
283    // An adapter's `.binding.ts` path is only known post-parse, so no discovery walk
284    // can pre-populate it into a caller-supplied overlay the way `.bynk` files are.
285    ("bynk-emit", "project/discovery.rs", "read_adapter_binding"),
286    // The plain, no-overlay manifest reader's contract has always been "read the real
287    // file"; nothing above it in the call chain can supply this for a caller that
288    // doesn't build its own overlay.
289    ("bynk-emit", "project/paths.rs", "try_read_project_paths"),
290];
291
292/// Is flagged file `files[i]` (already known, by [`production_std_fs_files`], to touch
293/// `std::fs` in production scope) a **named floor** file — every production-scope touch
294/// it has is either inside a [`NAMED_FS_EXCEPTIONS`] function for this exact
295/// `(krate, file)`, or a bare `use` import declaration (which reads but performs no
296/// filesystem operation by itself, unlike a module-scope `static`/`const` initialiser or
297/// macro invocation that might)? `facts`/`parents` are the caller's already-computed
298/// [`fs_import_facts`]/[`module_parent`] vectors for `files`, threaded through rather
299/// than recomputed per flagged file.
300///
301/// A single disallowed touch — inside an unlisted fn, inside a listed fn's *file* but
302/// wrong *name*, or outside every fn and not a plain import — makes the whole file
303/// residual: partial credit isn't meaningful here, since the point is "can a reader stop
304/// cross-referencing track docs for this file," not a ratio. Likewise, a file this
305/// function attributes *no* touch line to at all (despite the caller already knowing it's
306/// flagged — [`line_touches_std_fs`]'s re-implementation of the file-level detection
307/// disagreeing with it) reads as residual, not floor: an unattributable touch means this
308/// classifier doesn't understand the file, which must fail loud, not quiet.
309fn file_is_named_fs_floor(
310    krate: &str,
311    files: &[(PathBuf, String)],
312    facts: &[FsImportFacts],
313    parents: &[Option<usize>],
314    i: usize,
315) -> bool {
316    let (path, _) = &files[i];
317    let rel = path.to_string_lossy().replace('\\', "/");
318    let lines: Vec<&str> = files[i].1.lines().collect();
319    let ranges = test_mod_ranges(&lines);
320    let fn_ranges = production_fn_ranges(&lines, &ranges);
321
322    let mut saw_touch = false;
323    for (li, line) in lines.iter().enumerate() {
324        if in_test_range(li, &ranges) {
325            continue;
326        }
327        if !line_touches_std_fs(i, line, facts, parents, files) {
328            continue;
329        }
330        saw_touch = true;
331        let Some(fn_name) = enclosing_fn(li, &fn_ranges) else {
332            // No enclosing fn is harmless only when the line is literally an import
333            // declaration. A module-scope `static`/`const` initialiser, a macro
334            // invocation, or a fn shape `fn_name_on_line` can't parse (`extern "C" fn`)
335            // does real I/O outside every known range and must read as residual.
336            if use_declaration(line).is_some() {
337                continue;
338            }
339            return false;
340        };
341        let named = NAMED_FS_EXCEPTIONS
342            .iter()
343            .any(|&(c, f, func)| c == krate && f == rel && func == fn_name);
344        if !named {
345            return false;
346        }
347    }
348    saw_touch
349}
350
351/// Does `line` (already known to be production-scope) itself touch `std::fs` — by the
352/// same two means [`production_std_fs_files`] checks at file granularity, applied here
353/// to one line: a literal `std::fs` substring, or a bare/qualified path this line spells
354/// that resolves to `std::fs` through file `i`'s visible import bindings.
355fn line_touches_std_fs(
356    i: usize,
357    line: &str,
358    facts: &[FsImportFacts],
359    parents: &[Option<usize>],
360    files: &[(PathBuf, String)],
361) -> bool {
362    if line.contains("std::fs") {
363        return true;
364    }
365    let mut roots = BTreeSet::new();
366    collect_bare_path_roots(line, &mut roots);
367    if roots.iter().any(|name| {
368        matches!(
369            resolve_name_in_module(i, name, facts, parents),
370            NameResolution::StdFs
371        )
372    }) {
373        return true;
374    }
375    let mut chains = BTreeSet::new();
376    collect_qualified_paths(line, &mut chains);
377    chains
378        .iter()
379        .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
380}
381
382/// The name and inclusive body line-range of every production-scope `fn` in `lines`
383/// (`test_ranges` excluded, same as everywhere else in this probe) — used by
384/// [`file_is_named_fs_floor`] to attribute a flagged touch line to its enclosing
385/// function. A wrapped signature (the `{` arriving lines after the `fn` line, past a
386/// multi-line parameter list) is handled the same way [`test_mod_ranges`] handles a
387/// `mod` line: brace depth is tracked starting at the `fn` line itself, but a parameter
388/// list has no `{`/`}` in it, so `started` only flips true once the real body-opening
389/// brace arrives, however many lines later.
390fn production_fn_ranges(
391    lines: &[&str],
392    test_ranges: &[(usize, usize)],
393) -> Vec<(String, usize, usize)> {
394    let mut out = Vec::new();
395    for (i, line) in lines.iter().enumerate() {
396        if in_test_range(i, test_ranges) {
397            continue;
398        }
399        let Some(name) = fn_name_on_line(line) else {
400            continue;
401        };
402        let mut state = BraceScanState::Normal;
403        let mut depth = 0i32;
404        let mut started = false;
405        let mut end = lines.len() - 1;
406        for (j, l) in lines[i..].iter().enumerate() {
407            let (delta, new_state) = brace_delta(l, state);
408            state = new_state;
409            depth += delta;
410            if depth != 0 {
411                started = true;
412            }
413            if started && depth == 0 {
414                end = i + j;
415                break;
416            }
417        }
418        out.push((name, i, end));
419    }
420    out
421}
422
423/// The leading `fn NAME` on `line`, past an optional `pub`/`pub(...)`, `async`,
424/// `unsafe`, `const` modifier run (in any order/repetition, mirroring
425/// [`collect_declared_type_name`]'s `pub`-stripping) — `None` if `line` doesn't open a
426/// function at all (a call site, a doc comment mentioning "fn", a closure). Doesn't
427/// require a trailing `{` or even `(` on this same line — a wrapped signature's `fn`
428/// line can end right at the name.
429fn fn_name_on_line(line: &str) -> Option<String> {
430    let mut t = line.trim();
431    loop {
432        if let Some(rest) = t.strip_prefix("pub") {
433            let rest = rest.trim_start();
434            t = if let Some(after_paren) = rest.strip_prefix('(') {
435                after_paren.split_once(')')?.1.trim_start()
436            } else {
437                rest
438            };
439            continue;
440        }
441        let mut advanced = false;
442        for kw in ["async ", "unsafe ", "const "] {
443            if let Some(rest) = t.strip_prefix(kw) {
444                t = rest.trim_start();
445                advanced = true;
446                break;
447            }
448        }
449        if !advanced {
450            break;
451        }
452    }
453    let rest = t.strip_prefix("fn ")?;
454    let end = rest
455        .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
456        .unwrap_or(rest.len());
457    if end == 0 {
458        return None;
459    }
460    Some(rest[..end].to_string())
461}
462
463/// The innermost [`production_fn_ranges`] entry containing `line_idx`, by name — `None`
464/// if `line_idx` sits outside every production fn (module scope: a `use` declaration,
465/// a `const`/`static`, or a `struct`/`enum` body).
466fn enclosing_fn(line_idx: usize, fn_ranges: &[(String, usize, usize)]) -> Option<String> {
467    fn_ranges
468        .iter()
469        .filter(|(_, start, end)| line_idx >= *start && line_idx <= *end)
470        .min_by_key(|(_, start, end)| end - start)
471        .map(|(name, _, _)| name.clone())
472}
473
474/// The literal text component of [`fs_below_driver`]: some production-scope line names
475/// `std::fs`. Necessary but not sufficient (#1013) — a file can touch `std::fs`
476/// through a glob-imported parent binding without ever spelling it; that resolution
477/// lives in [`production_std_fs_files`], which layers on top of this scan.
478fn has_production_std_fs(src: &str) -> bool {
479    let lines: Vec<&str> = src.lines().collect();
480    let ranges = test_mod_ranges(&lines);
481    for (i, line) in lines.iter().enumerate() {
482        if in_test_range(i, &ranges) {
483            continue;
484        }
485        if line.contains("std::fs") {
486            return true;
487        }
488    }
489    false
490}
491
492/// Indices (into `files`, whose paths are relative to the crate's `src/` root) of the
493/// files that touch `std::fs` in production code — the union of the literal text scan
494/// ([`has_production_std_fs`]) and import resolution: a path whose leading module
495/// segment a production `use` declaration binds to `std::fs` (or an item under it),
496/// either a bare `NAME::` root resolved in the file itself (`use std::{fs, io};` — a
497/// form the substring scan can't see) or in an ancestor module reached through
498/// `use super::*;`, transitively (#1013), or a `super::`/`self::`/`crate::`-qualified
499/// path walked through the module tree to the same bindings (#1016 review — a
500/// descendant may spell `super::fs::read_to_string(p)` with no glob import at all,
501/// one disambiguating edit away from a currently-flagged bare call).
502///
503/// Resolution is Rust-shaped, not hand-tracked (#1013 rejects a special-case list):
504/// a private `use std::fs;` in a parent is visible to descendants because module
505/// privacy is ancestor-scoped, a chain of `use super::*;` globs re-reaches it from
506/// any depth, and a nearer binding of the same name shadows a farther one — whether
507/// that binding is another `use` or a locally-declared type-namespace item (`mod fs;`,
508/// `struct File`, …; value-namespace items like `fn` can't head a `NAME::` path, so
509/// they don't shadow one) — so a child that binds `fs` to something else keeps its
510/// bare `fs::` calls unflagged. Visibility is *not* modelled: a path that names a
511/// too-private binding wouldn't compile anyway, so over-approximating is safe.
512///
513/// Known remaining gaps, accepted as out of reach for a text-level scanner: an
514/// ancestor's `use std::fs::read_to_string;` item import called bare (`read_to_string(p)`)
515/// presents no `::` path segment to resolve — the same import used as a path root
516/// (`File::open`) **is** caught, since item bindings under `std::fs` participate in
517/// the same resolution — and a `use` declaration rustfmt has split across lines is
518/// not parsed. #1013 grepped the three scanned crates for the item-import form, and
519/// the #1016 review for the qualified-path and split-declaration forms — zero hits.
520fn production_std_fs_files(files: &[(PathBuf, String)]) -> Vec<usize> {
521    let facts: Vec<FsImportFacts> = files.iter().map(|(_, src)| fs_import_facts(src)).collect();
522    let parents: Vec<Option<usize>> = files
523        .iter()
524        .map(|(path, _)| module_parent(path, files))
525        .collect();
526    (0..files.len())
527        .filter(|&i| {
528            has_production_std_fs(&files[i].1)
529                || resolves_bare_std_fs(i, &facts, &parents)
530                || resolves_qualified_std_fs(i, &facts, &parents, files)
531        })
532        .collect()
533}
534
535/// Per-file production-scope import facts for [`production_std_fs_files`]'s
536/// resolution. All fields exclude `#[cfg(test)] mod` ranges — a test module's
537/// `use super::*;` or tempdir `fs::write` must not make the file, or its children,
538/// read as production `std::fs` (the `bynk-ide` files' shape).
539#[derive(Default)]
540struct FsImportFacts {
541    /// A production `use super::*;` (optionally `pub`-qualified) — the edge that lets
542    /// this file see its parent module's `use` bindings, and (chained) its ancestors'.
543    glob_imports_super: bool,
544    /// Names production `use` declarations bind to `std::fs` or an item under it:
545    /// `use std::fs;` → `fs`, `use std::fs as x;` → `x`, `use std::{fs, io};` → `fs`,
546    /// `use std::fs::File;` → `File`.
547    std_fs_bindings: BTreeSet<String>,
548    /// Every name a production `use` declaration binds, whatever the target — the
549    /// shadow set: a nearer non-`std::fs` binding of a candidate name stops resolution.
550    use_bound_names: BTreeSet<String>,
551    /// Type-namespace items the file declares (`mod fs;`, `struct File`, `enum`,
552    /// `trait`, `type`, `union`) — these beat a glob-imported name in real Rust, so
553    /// they join [`Self::use_bound_names`] on the shadow side of resolution (#1016
554    /// review). Value-namespace items (`fn`, `const`, `static`) can't head a `NAME::`
555    /// module path and are deliberately not collected.
556    declared_type_names: BTreeSet<String>,
557    /// Identifiers appearing as a bare path root `NAME::` (not preceded by another
558    /// path segment) on a production line — the call-site side of the resolution.
559    bare_path_roots: BTreeSet<String>,
560    /// Segment chains of `super::`/`self::`/`crate::`-qualified paths on production
561    /// lines — `super::fs::read_to_string` records `["super", "fs", "read_to_string"]`.
562    /// These need no glob import to reach an ancestor's binding (#1016 review).
563    qualified_paths: BTreeSet<Vec<String>>,
564}
565
566fn fs_import_facts(src: &str) -> FsImportFacts {
567    let lines: Vec<&str> = src.lines().collect();
568    let ranges = test_mod_ranges(&lines);
569    let mut facts = FsImportFacts::default();
570    for (i, line) in lines.iter().enumerate() {
571        if in_test_range(i, &ranges) {
572            continue;
573        }
574        if let Some(decl) = use_declaration(line) {
575            if decl == "super::*" {
576                facts.glob_imports_super = true;
577            }
578            collect_use_bindings("", decl, &mut facts);
579        }
580        collect_declared_type_name(line, &mut facts.declared_type_names);
581        collect_bare_path_roots(line, &mut facts.bare_path_roots);
582        collect_qualified_paths(line, &mut facts.qualified_paths);
583    }
584    facts
585}
586
587/// The path text of a single-line `use` declaration — `use std::fs;` → `std::fs`,
588/// with an optional `pub`/`pub(crate)`/`pub(in …)` prefix stripped and a trailing
589/// `//` comment tolerated (`use super::*; // parent's fs` must not silently sever
590/// the glob edge for a whole subtree — #1016 review; safe to split on `//` because
591/// a `use` path can contain neither a comment marker nor a string). A declaration
592/// rustfmt has split across lines has no trailing `;` here and is not recognised —
593/// none of the `std::fs` forms in the scanned crates are long enough to split.
594fn use_declaration(line: &str) -> Option<&str> {
595    let mut t = line.trim();
596    if let Some(rest) = t.strip_prefix("pub") {
597        let rest = rest.trim_start();
598        t = if let Some(after_paren) = rest.strip_prefix('(') {
599            after_paren.split_once(')')?.1.trim_start()
600        } else {
601            rest
602        };
603    }
604    let body = t.strip_prefix("use ")?;
605    let body = body.split("//").next().unwrap_or(body);
606    body.trim().strip_suffix(';').map(str::trim)
607}
608
609/// If `line` declares a type-namespace item — `mod`/`struct`/`enum`/`trait`/`type`/
610/// `union`, optionally `pub`-qualified, optionally `unsafe` (traits) — record its
611/// name. Field/variable positions can't start a trimmed line with these keywords, so
612/// a leading-keyword scan is enough for rustfmt-shaped code.
613fn collect_declared_type_name(line: &str, out: &mut BTreeSet<String>) {
614    let mut t = line.trim();
615    if let Some(rest) = t.strip_prefix("pub") {
616        let rest = rest.trim_start();
617        t = if let Some(after_paren) = rest.strip_prefix('(') {
618            match after_paren.split_once(')') {
619                Some((_, after)) => after.trim_start(),
620                None => return,
621            }
622        } else {
623            rest
624        };
625    }
626    if let Some(rest) = t.strip_prefix("unsafe ") {
627        t = rest.trim_start();
628    }
629    for kw in ["mod ", "struct ", "enum ", "trait ", "type ", "union "] {
630        if let Some(rest) = t.strip_prefix(kw) {
631            let rest = rest.trim_start();
632            let end = rest
633                .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
634                .unwrap_or(rest.len());
635            if end > 0 {
636                out.insert(rest[..end].to_string());
637            }
638            return;
639        }
640    }
641}
642
643/// Record the name(s) a `use` path binds into `facts` — `path::to::name`,
644/// `path as alias`, and brace groups (`std::{fs, path::PathBuf}`, nested one level
645/// per recursion). `prefix` is the already-consumed leading path (empty at the top).
646fn collect_use_bindings(prefix: &str, entry: &str, facts: &mut FsImportFacts) {
647    let entry = entry.trim();
648    if entry.is_empty() {
649        return;
650    }
651    if let Some((path_part, group)) = entry.split_once('{') {
652        let inner_prefix = join_use_path(prefix, path_part.trim().trim_end_matches("::"));
653        let group = group.strip_suffix('}').unwrap_or(group);
654        for part in split_group_entries(group) {
655            collect_use_bindings(&inner_prefix, part, facts);
656        }
657        return;
658    }
659    let (path_part, alias) = match entry.split_once(" as ") {
660        Some((p, a)) => (p.trim(), Some(a.trim())),
661        None => (entry, None),
662    };
663    let full = join_use_path(prefix, path_part);
664    // `use std::fs::{self};` binds `fs` — normalise the `self` leaf away.
665    let full = full.strip_suffix("::self").unwrap_or(&full);
666    let last = full.rsplit("::").next().unwrap_or(full);
667    let name = alias.unwrap_or(last);
668    if name.is_empty() || name == "*" {
669        return; // globs bind no single name; `super::*` is tracked separately
670    }
671    facts.use_bound_names.insert(name.to_string());
672    if full == "std::fs" || full.starts_with("std::fs::") {
673        facts.std_fs_bindings.insert(name.to_string());
674    }
675}
676
677fn join_use_path(prefix: &str, part: &str) -> String {
678    if prefix.is_empty() {
679        part.to_string()
680    } else {
681        format!("{prefix}::{part}")
682    }
683}
684
685/// Split a brace group's contents on top-level commas only — `fs::{self, File}, io`
686/// is two entries, not three.
687fn split_group_entries(s: &str) -> Vec<&str> {
688    let mut out = Vec::new();
689    let mut depth = 0i32;
690    let mut start = 0;
691    for (i, c) in s.char_indices() {
692        match c {
693            '{' => depth += 1,
694            '}' => depth -= 1,
695            ',' if depth == 0 => {
696                out.push(&s[start..i]);
697                start = i + 1;
698            }
699            _ => {}
700        }
701    }
702    out.push(&s[start..]);
703    out
704}
705
706/// Every identifier `NAME` occurring as `NAME::` where the character before `NAME` is
707/// not `:` — i.e. a path *root*, so `std::fs::read` contributes `std`, never `fs`.
708/// Same line discipline as the text scan: comments included, production scope only
709/// (the caller has already excluded test ranges).
710fn collect_bare_path_roots(line: &str, out: &mut BTreeSet<String>) {
711    let bytes = line.as_bytes();
712    let mut search_from = 0;
713    while let Some(rel) = line[search_from..].find("::") {
714        let pos = search_from + rel;
715        let mut start = pos;
716        while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') {
717            start -= 1;
718        }
719        if start < pos && (start == 0 || bytes[start - 1] != b':') {
720            out.insert(line[start..pos].to_string());
721        }
722        search_from = pos + 2;
723    }
724}
725
726/// Every `super::`/`self::`/`crate::`-rooted path on `line`, as its segment chain —
727/// `super::fs::read_to_string(p)` yields `["super", "fs", "read_to_string"]`. The
728/// root must sit at a bare word boundary (not `a_super::` or `a::super::`), so only
729/// genuine path roots are collected; `Self::` (capital) never matches, and `self.x`
730/// has no `::` to match.
731fn collect_qualified_paths(line: &str, out: &mut BTreeSet<Vec<String>>) {
732    let bytes = line.as_bytes();
733    for root in ["super", "self", "crate"] {
734        let mut from = 0;
735        while let Some(rel) = line[from..].find(root) {
736            let start = from + rel;
737            let root_end = start + root.len();
738            from = root_end;
739            let boundary_ok = start == 0 || {
740                let c = bytes[start - 1];
741                !(c.is_ascii_alphanumeric() || c == b'_' || c == b':')
742            };
743            if !boundary_ok || !line[root_end..].starts_with("::") {
744                continue;
745            }
746            let mut segments = vec![root.to_string()];
747            let mut pos = root_end;
748            while line[pos..].starts_with("::") {
749                let seg_start = pos + 2;
750                let mut seg_end = seg_start;
751                while seg_end < bytes.len()
752                    && (bytes[seg_end].is_ascii_alphanumeric() || bytes[seg_end] == b'_')
753                {
754                    seg_end += 1;
755                }
756                if seg_end == seg_start {
757                    break; // `super::*` and friends — no further identifier
758                }
759                segments.push(line[seg_start..seg_end].to_string());
760                pos = seg_end;
761            }
762            if segments.len() >= 2 {
763                out.insert(segments);
764            }
765        }
766    }
767}
768
769/// The file defining `path`'s parent module, by the standard layout: `a/b.rs`'s parent
770/// is `a.rs` (or `a/mod.rs`), `a/mod.rs`'s parent is the crate root, and the roots
771/// (`lib.rs`/`main.rs`) have none. `#[path]`-remapped modules are not handled — none
772/// exist below the driver, and a text-level probe can't chase them anyway.
773fn module_parent(path: &Path, files: &[(PathBuf, String)]) -> Option<usize> {
774    let stem = path.file_stem()?.to_str()?;
775    let dir = path.parent().filter(|d| !d.as_os_str().is_empty());
776    let parent_module: PathBuf = if stem == "mod" {
777        dir?.parent().map(Path::to_path_buf).unwrap_or_default()
778    } else if let Some(dir) = dir {
779        dir.to_path_buf()
780    } else {
781        if stem == "lib" || stem == "main" {
782            return None;
783        }
784        PathBuf::new()
785    };
786    let candidates = if parent_module.as_os_str().is_empty() {
787        vec![PathBuf::from("lib.rs"), PathBuf::from("main.rs")]
788    } else {
789        vec![
790            parent_module.with_extension("rs"),
791            parent_module.join("mod.rs"),
792        ]
793    };
794    candidates
795        .iter()
796        .find_map(|c| files.iter().position(|(p, _)| p == c))
797}
798
799/// The scopes whose bindings a name used in module `i` can see: the module itself,
800/// then each ancestor reachable while every module below it glob-imports `super::*`.
801fn visible_scopes(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> Vec<usize> {
802    let mut scopes = vec![i];
803    let mut cur = i;
804    loop {
805        if !facts[cur].glob_imports_super {
806            break;
807        }
808        let Some(parent) = parents[cur] else { break };
809        scopes.push(parent);
810        cur = parent;
811    }
812    scopes
813}
814
815/// How `name` resolves in module `m`'s namespace, walking [`visible_scopes`] with
816/// nearest binding winning — a closer non-`std::fs` `use` binding *or* locally
817/// declared type-namespace item shadows a farther `std::fs` binding, as in Rust.
818enum NameResolution {
819    StdFs,
820    Other,
821    Unbound,
822}
823
824fn resolve_name_in_module(
825    m: usize,
826    name: &str,
827    facts: &[FsImportFacts],
828    parents: &[Option<usize>],
829) -> NameResolution {
830    for s in visible_scopes(m, facts, parents) {
831        if facts[s].std_fs_bindings.contains(name) {
832            return NameResolution::StdFs;
833        }
834        if facts[s].use_bound_names.contains(name) || facts[s].declared_type_names.contains(name) {
835            return NameResolution::Other;
836        }
837    }
838    NameResolution::Unbound
839}
840
841/// Does a bare path root in file `i` resolve to `std::fs` through the bindings it
842/// can see? Candidates are the names any visible scope binds to `std::fs`; each is
843/// then resolved from `i` with nearest-binding-wins shadowing.
844fn resolves_bare_std_fs(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> bool {
845    let scopes = visible_scopes(i, facts, parents);
846    let mut candidates: BTreeSet<&str> = BTreeSet::new();
847    for &s in &scopes {
848        candidates.extend(facts[s].std_fs_bindings.iter().map(String::as_str));
849    }
850    candidates.into_iter().any(|name| {
851        facts[i].bare_path_roots.contains(name)
852            && matches!(
853                resolve_name_in_module(i, name, facts, parents),
854                NameResolution::StdFs
855            )
856    })
857}
858
859/// Does a `super::`/`self::`/`crate::`-qualified path in file `i` reach a `std::fs`
860/// binding (#1016 review)? Unlike the bare-root case these need no glob import: the
861/// root picks the starting module directly (`super`-hops up the parent chain, `self`
862/// the file itself, `crate` the crate root), then each further segment either
863/// resolves in that module's namespace — `std::fs` flags, anything else stops — or
864/// descends into a child module file and continues. Inline `mod name { … }` blocks
865/// are not modelled (their `use` bindings live in the same file, which the text scan
866/// and bare-root resolution already cover).
867fn resolves_qualified_std_fs(
868    i: usize,
869    facts: &[FsImportFacts],
870    parents: &[Option<usize>],
871    files: &[(PathBuf, String)],
872) -> bool {
873    facts[i]
874        .qualified_paths
875        .iter()
876        .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
877}
878
879fn qualified_chain_reaches_std_fs(
880    chain: &[String],
881    i: usize,
882    facts: &[FsImportFacts],
883    parents: &[Option<usize>],
884    files: &[(PathBuf, String)],
885) -> bool {
886    let mut idx = 1;
887    let mut m = match chain[0].as_str() {
888        "self" => i,
889        "crate" => {
890            let root = files
891                .iter()
892                .position(|(p, _)| p == Path::new("lib.rs") || p == Path::new("main.rs"));
893            match root {
894                Some(root) => root,
895                None => return false,
896            }
897        }
898        "super" => {
899            let mut m = i;
900            idx = 0;
901            while idx < chain.len() && chain[idx] == "super" {
902                let Some(parent) = parents[m] else {
903                    return false;
904                };
905                m = parent;
906                idx += 1;
907            }
908            m
909        }
910        _ => return false,
911    };
912    while idx < chain.len() {
913        let seg = chain[idx].as_str();
914        // Resolve `seg` in `m`, nearest scope first. Within a scope, a child module
915        // file for `seg` is checked *before* the shadow set: a declared `mod seg;`
916        // lands `seg` in `declared_type_names`, but that declaration IS the child
917        // module — it's the path's next hop, not a shadow over it. (In valid Rust a
918        // module and another same-name type-namespace item can't coexist in one
919        // scope, so the ordering costs nothing.)
920        let mut next = None;
921        for s in visible_scopes(m, facts, parents) {
922            if facts[s].std_fs_bindings.contains(seg) {
923                return true;
924            }
925            if let Some(child) = child_module_file(s, seg, files) {
926                next = Some(child);
927                break;
928            }
929            if facts[s].use_bound_names.contains(seg) || facts[s].declared_type_names.contains(seg)
930            {
931                return false; // bound to something that is neither std::fs nor a module
932            }
933        }
934        let Some(child) = next else {
935            return false;
936        };
937        m = child;
938        idx += 1;
939    }
940    false
941}
942
943/// The file defining module `m`'s child module `seg`, if it exists as a file:
944/// `lib.rs` + `a` → `a.rs`/`a/mod.rs`, `a.rs` + `b` → `a/b.rs`/`a/b/mod.rs`,
945/// `a/mod.rs` + `b` → `a/b.rs`/`a/b/mod.rs`.
946fn child_module_file(m: usize, seg: &str, files: &[(PathBuf, String)]) -> Option<usize> {
947    let m_path = &files[m].0;
948    let module_dir: PathBuf = match m_path.file_stem().and_then(|s| s.to_str()) {
949        Some("mod") => m_path.parent().unwrap_or(Path::new("")).to_path_buf(),
950        Some("lib") | Some("main") if m_path.parent().is_none_or(|p| p.as_os_str().is_empty()) => {
951            PathBuf::new()
952        }
953        _ => m_path.with_extension(""),
954    };
955    let candidates = [
956        module_dir.join(format!("{seg}.rs")),
957        module_dir.join(seg).join("mod.rs"),
958    ];
959    candidates
960        .iter()
961        .find_map(|c| files.iter().position(|(p, _)| p == c))
962}
963
964/// Every `#[cfg(test)] mod <ident> { ... }` block in `lines`, as inclusive
965/// `(start_line, end_line)` line-index ranges — every occurrence, not just a single
966/// trailing block. A file in this codebase can carry several test modules scattered
967/// through it with production code between them — `bynk-emit/src/emitter/lower.rs` has
968/// two, 1031 lines apart, and treating "everything after the first (or last)
969/// `#[cfg(test)]`" as one cutoff silently misclassifies that intervening production
970/// code as test-scope (caught in review: it made `fs_below_driver`, a *gated* probe,
971/// blind over that span, and inflated `test_density`'s ratio by up to 39%).
972///
973/// A block's end is found by real brace-depth counting via [`brace_delta`], not a
974/// "first column-0 `}`" shortcut: an earlier version of this fix tried exactly that
975/// shortcut (reasoning that rustfmt always dedents a closing brace back to column 0)
976/// and it broke on files like `bynk-ide/src/sequence.rs`, whose test module embeds
977/// multi-line `.bynk`/TypeScript fixture source as string literals — source that
978/// itself contains a column-0 `}` closing a top-level construct *inside the string*,
979/// which the shortcut mistook for the end of the Rust `mod` block, truncating it by
980/// hundreds of lines. `brace_delta` skips characters inside Rust string/char literals
981/// and comments, so embedded fixture text can't be mistaken for real Rust braces.
982///
983/// Only matches a brace-opening `mod` line — `#[cfg(test)] mod foo;` (an external-file
984/// declaration, not an inline scope) does not open a range.
985fn test_mod_ranges(lines: &[&str]) -> Vec<(usize, usize)> {
986    let mut ranges = Vec::new();
987    let mut i = 0;
988    while i < lines.len() {
989        if lines[i].trim() == "#[cfg(test)]"
990            && let Some(off) = lines[i + 1..].iter().position(|l| !l.trim().is_empty())
991        {
992            let mod_line = i + 1 + off;
993            let t = lines[mod_line].trim();
994            if t.starts_with("mod ") && t.ends_with('{') {
995                let mut depth = 0i32;
996                let mut state = BraceScanState::Normal;
997                let mut started = false;
998                let mut end = lines.len() - 1;
999                for (j, line) in lines[mod_line..].iter().enumerate() {
1000                    let (delta, new_state) = brace_delta(line, state);
1001                    state = new_state;
1002                    depth += delta;
1003                    if depth != 0 {
1004                        started = true;
1005                    }
1006                    if started && depth == 0 {
1007                        end = mod_line + j;
1008                        break;
1009                    }
1010                }
1011                ranges.push((mod_line, end));
1012                i = end + 1;
1013                continue;
1014            }
1015        }
1016        i += 1;
1017    }
1018    ranges
1019}
1020
1021fn in_test_range(line_idx: usize, ranges: &[(usize, usize)]) -> bool {
1022    ranges
1023        .iter()
1024        .any(|(start, end)| line_idx >= *start && line_idx <= *end)
1025}
1026
1027/// Scanner state carried across lines for [`brace_delta`]: whether the cursor is
1028/// inside a string literal, a raw string (with its `#`-count), or a block comment
1029/// (with nesting depth — Rust block comments nest).
1030#[derive(Clone, Copy, PartialEq)]
1031enum BraceScanState {
1032    Normal,
1033    InString,
1034    InRawString(u8),
1035    InBlockComment(u32),
1036}
1037
1038/// The net `{`/`}` depth change in `line`, skipping characters inside Rust string/char
1039/// literals, raw strings, and line/block comments — a naive per-character brace count
1040/// breaks the moment a line contains a fixture string like `"fn f() { \"{\" }"` or a
1041/// doc comment mentioning a brace. Returns the depth delta and the state to carry into
1042/// the next line (a string or block comment can span line boundaries).
1043fn brace_delta(line: &str, mut state: BraceScanState) -> (i32, BraceScanState) {
1044    let mut delta = 0i32;
1045    let chars: Vec<char> = line.chars().collect();
1046    let mut i = 0;
1047    while i < chars.len() {
1048        match state {
1049            BraceScanState::Normal => {
1050                if chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
1051                    break; // rest of the line is a line comment
1052                }
1053                if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1054                    state = BraceScanState::InBlockComment(1);
1055                    i += 2;
1056                    continue;
1057                }
1058                if chars[i] == '"' {
1059                    state = BraceScanState::InString;
1060                    i += 1;
1061                    continue;
1062                }
1063                if chars[i] == 'r' && matches!(chars.get(i + 1), Some('"') | Some('#')) {
1064                    let mut j = i + 1;
1065                    let mut hashes = 0u8;
1066                    while chars.get(j) == Some(&'#') {
1067                        hashes += 1;
1068                        j += 1;
1069                    }
1070                    if chars.get(j) == Some(&'"') {
1071                        state = BraceScanState::InRawString(hashes);
1072                        i = j + 1;
1073                        continue;
1074                    }
1075                }
1076                if chars[i] == '\'' {
1077                    // A `'\x'`/`'\\'`-style escaped char literal, or a plain `'x'` —
1078                    // skip past it so its contents can't be mistaken for braces.
1079                    // Anything else (no closing `'` within a couple of chars) is a
1080                    // lifetime, which owns no closing quote to skip.
1081                    if chars.get(i + 1) == Some(&'\\') {
1082                        let mut j = i + 2;
1083                        while j < chars.len() && chars[j] != '\'' {
1084                            j += 1;
1085                        }
1086                        i = (j + 1).min(chars.len());
1087                        continue;
1088                    } else if chars.get(i + 2) == Some(&'\'') {
1089                        i += 3;
1090                        continue;
1091                    }
1092                }
1093                match chars[i] {
1094                    '{' => delta += 1,
1095                    '}' => delta -= 1,
1096                    _ => {}
1097                }
1098                i += 1;
1099            }
1100            BraceScanState::InString => {
1101                if chars[i] == '\\' {
1102                    i += 2;
1103                    continue;
1104                }
1105                if chars[i] == '"' {
1106                    state = BraceScanState::Normal;
1107                }
1108                i += 1;
1109            }
1110            BraceScanState::InRawString(hashes) => {
1111                if chars[i] == '"' {
1112                    let mut j = i + 1;
1113                    let mut h = 0u8;
1114                    while chars.get(j) == Some(&'#') && h < hashes {
1115                        h += 1;
1116                        j += 1;
1117                    }
1118                    if h == hashes {
1119                        state = BraceScanState::Normal;
1120                        i = j;
1121                        continue;
1122                    }
1123                }
1124                i += 1;
1125            }
1126            BraceScanState::InBlockComment(depth) => {
1127                if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1128                    state = BraceScanState::InBlockComment(depth + 1);
1129                    i += 2;
1130                    continue;
1131                }
1132                if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
1133                    state = if depth <= 1 {
1134                        BraceScanState::Normal
1135                    } else {
1136                        BraceScanState::InBlockComment(depth - 1)
1137                    };
1138                    i += 2;
1139                    continue;
1140                }
1141                i += 1;
1142            }
1143        }
1144    }
1145    (delta, state)
1146}
1147
1148// --- Gated probe 3: options_sources --------------------------------------
1149
1150/// R2.3. `CompileOptions` (in `bynk-emit/src/project.rs`) has a `sources` field.
1151fn options_sources(root: &Path) -> Probe {
1152    let src = std::fs::read_to_string(root.join("bynk-emit/src/project.rs")).unwrap_or_default();
1153    let present = struct_body(&src, "CompileOptions").is_some_and(|body| body.contains("sources"));
1154    Probe {
1155        name: "options_sources",
1156        gated: true,
1157        reads: if present {
1158            "present".to_string()
1159        } else {
1160            "absent".to_string()
1161        },
1162    }
1163}
1164
1165/// The `{ ... }` body text of `struct <name>` in `src`, brace-matched from the struct's
1166/// own opening brace to its close.
1167fn struct_body<'a>(src: &'a str, name: &str) -> Option<&'a str> {
1168    let needle = format!("struct {name}");
1169    let start = src.find(&needle)?;
1170    let open = start + src[start..].find('{')?;
1171    let mut depth = 0i32;
1172    for (offset, ch) in src[open..].char_indices() {
1173        match ch {
1174            '{' => depth += 1,
1175            '}' => {
1176                depth -= 1;
1177                if depth == 0 {
1178                    return Some(&src[open..open + offset + 1]);
1179                }
1180            }
1181            _ => {}
1182        }
1183    }
1184    None
1185}
1186
1187// --- Gated probe 4: hoist_sinks -------------------------------------------
1188
1189/// R6.2. Live (non-comment) occurrences of the sink-passing signature
1190/// `stmts: &mut Vec<String>` in `bynk-emit`. Tier B (T2.1) deletes it entirely.
1191fn hoist_sinks(root: &Path) -> Probe {
1192    let dir = root.join("bynk-emit/src");
1193    let needle = "stmts: &mut Vec<String>";
1194    let mut count = 0usize;
1195    for (_, contents) in rust_files(&dir) {
1196        for line in contents.lines() {
1197            if !is_line_comment(line) && line.contains(needle) {
1198                count += 1;
1199            }
1200        }
1201    }
1202    Probe {
1203        name: "hoist_sinks",
1204        gated: true,
1205        reads: count.to_string(),
1206    }
1207}
1208
1209// --- Gated probe 5: span_keyed_maps ---------------------------------------
1210
1211/// R2.4. Whole-repo occurrences of `HashMap<Span` (comments included — the phase-3
1212/// migration target is every mention, not just live call sites), **excluding
1213/// `xtask` itself**: this probe's own doc comment and source both name the search
1214/// string, which would otherwise self-count every time this file is touched — the
1215/// same self-reference hazard flagged for the dead-identifier probes below, caught
1216/// here by running the probe against itself before committing the first table.
1217fn span_keyed_maps(root: &Path) -> Probe {
1218    let count = count_repo_wide(root, "HashMap<Span", &["xtask"]);
1219    Probe {
1220        name: "span_keyed_maps",
1221        gated: true,
1222        reads: count.to_string(),
1223    }
1224}
1225
1226fn count_repo_wide(root: &Path, needle: &str, exclude_crates: &[&str]) -> usize {
1227    let mut total = 0usize;
1228    for entry in top_level_crate_dirs(root) {
1229        if exclude_crates
1230            .iter()
1231            .any(|c| entry.file_name().is_some_and(|n| n == *c))
1232        {
1233            continue;
1234        }
1235        for (_, contents) in rust_files(&entry.join("src")) {
1236            total += contents.matches(needle).count();
1237        }
1238    }
1239    total
1240}
1241
1242/// Every workspace member crate directory (anything at the repo root with a
1243/// `Cargo.toml` and a `src/` dir), excluding `target` and non-crate directories.
1244fn top_level_crate_dirs(root: &Path) -> Vec<PathBuf> {
1245    let mut out = Vec::new();
1246    let Ok(entries) = std::fs::read_dir(root) else {
1247        return out;
1248    };
1249    for entry in entries.flatten() {
1250        let path = entry.path();
1251        if path.is_dir() && path.join("Cargo.toml").is_file() && path.join("src").is_dir() {
1252            out.push(path);
1253        }
1254    }
1255    out.sort();
1256    out
1257}
1258
1259// --- Gated probe 6: emit_diagnostics --------------------------------------
1260
1261/// R3.5. `bynk.*` string literals in `bynk-emit`/`bynk-check` source, cross-referenced
1262/// against `bynk_syntax::diagnostics::REGISTRY` — not pattern-matched. A literal not in
1263/// `REGISTRY` is a commons/namespace path (e.g. `bynk.locale`, the compiled first-party
1264/// source module name), not a diagnostic code, and must not inflate the count (#999
1265/// Decision A: this cross-reference is what makes the exclusion correct by
1266/// construction rather than a second hand-maintained list).
1267fn emit_diagnostics(root: &Path) -> Probe {
1268    let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
1269        .iter()
1270        .map(|d| d.code)
1271        .collect();
1272    let mut parts = Vec::new();
1273    for (label, dir) in [
1274        ("bynk-emit", "bynk-emit/src"),
1275        ("bynk-check", "bynk-check/src"),
1276    ] {
1277        let mut naive: BTreeSet<String> = BTreeSet::new();
1278        for (_, contents) in rust_files(&root.join(dir)) {
1279            for lit in bynk_dotted_literals(&contents) {
1280                naive.insert(lit.to_string());
1281            }
1282        }
1283        let true_count = naive
1284            .iter()
1285            .filter(|l| registry.contains(l.as_str()))
1286            .count();
1287        parts.push(format!("{label}={true_count}/{}", naive.len()));
1288    }
1289    Probe {
1290        name: "emit_diagnostics",
1291        gated: true,
1292        reads: format!("{} (true/naive)", parts.join(", ")),
1293    }
1294}
1295
1296// --- Gated probe 7: ide_emit_edge -----------------------------------------
1297
1298/// R10.2. `bynk-ide` → `bynk-emit` in the manifest (`bynk-emit.workspace = true` or an
1299/// equivalent path/version dependency line).
1300fn ide_emit_edge(root: &Path) -> Probe {
1301    let manifest = std::fs::read_to_string(root.join("bynk-ide/Cargo.toml")).unwrap_or_default();
1302    let present = manifest
1303        .lines()
1304        .any(|l| l.trim_start().starts_with("bynk-emit"));
1305    Probe {
1306        name: "ide_emit_edge",
1307        gated: true,
1308        reads: if present {
1309            "present".to_string()
1310        } else {
1311            "absent".to_string()
1312        },
1313    }
1314}
1315
1316// --- Gated probe 8: ast_importers -----------------------------------------
1317
1318/// #1176: `bynk-emit::ir`'s own two files — named exactly, not by path prefix, the same
1319/// permanent-carve-out discipline [`NAMED_FS_EXCEPTIONS`] and [`emit_diagnostics`]'s
1320/// registry cross-reference already use. An `Ast → Ir` lowering pass importing
1321/// `bynk_syntax::ast` is that pass's entire job, not the AST-walking this track is
1322/// closing (`the-ir.md` §5's own P6.9 correction, #1167) — but `project.rs` also
1323/// imports `bynk_syntax::ast` today (`EmitProjectCtx` holding `ActorDecl`/`AgentDecl`
1324/// fields directly), and that *is* exactly the still-open R6.13 defect this probe
1325/// tracks (P6.6: "closes the emitter reading AST declarations directly"). A
1326/// path-prefix rule scoped to `emitter/**` would exclude that file right along with
1327/// `ir/`'s legitimate ones, silently undercounting real remaining work — see
1328/// [`is_named_ast_importer`].
1329///
1330/// #1184 review: this exclusion is necessary but not sufficient for R6.13. `ir.rs`
1331/// itself still holds several AST types directly in `IrItem`-adjacent struct fields
1332/// (`Arc<TypeDecl>`, `Arc<FnDecl>`, `HandlerKind`, `Refinement`, `SchemaVersionPattern`)
1333/// rather than IR-native equivalents — an emitter reading e.g. `IrHandler::kind`, which
1334/// *is* `ast::HandlerKind`, touches the AST without ever spelling `bynk_syntax::ast`
1335/// itself, so it is invisible to this probe by construction. `ast_importers` = 0 proves
1336/// no *remaining* file outside these two imports the AST module directly; it does not
1337/// by itself prove every `IrItem` field is AST-free (`the-ir.md` §5's own added note).
1338///
1339/// #1187's own closing scoping pass adds one more, on different grounds than the
1340/// `ir.rs`/`ir/lower.rs` pair above: `project/tests_emit.rs` was deliberately *not*
1341/// added alongside `project.rs` when this list was first cut (the
1342/// `ast_importer_exclusion_is_named_not_prefixed` test below used to assert exactly
1343/// that) — #1187's own scoping pass found new evidence changing that: its test/suite
1344/// case bodies call `emitter::lower_block_to_async_body`/`lower_test_case_body`/
1345/// `lower_integration_case_body` directly (the Q7-settled body-rendering pass,
1346/// `the-ir.md` §3.7 — `emitter/lower.rs` keeps hand-writing TypeScript source text
1347/// after this track's cutover, the printer that would change that is phase 7's), and
1348/// its own `driver_param_ty`/`strip_effect_httpresult` read a handler's *declared*
1349/// param/return `TypeRef` with no corresponding `TyId` available at that call site
1350/// (the same caller-reads-callee's-raw-declared-shape pattern #661 established for
1351/// cross-context codec generation). Both are the Q7/printer kind of unreachable, not
1352/// the "still open, real work" kind the original exclusion list deliberately left this
1353/// file out of — the correction is new evidence, not a reversal of that reasoning.
1354///
1355/// Review of #1210: `emitter.rs`/`emitter/lower.rs` themselves were considered for
1356/// this same exclusion and **rejected** — Q7 settles that these files' *body-rendering*
1357/// surface stays AST-parameter-driven, but both files also hold live, currently
1358/// untouched AST-*declaration* reads with no such gate: `emitter.rs`'s own
1359/// `CommonsItem::Service`/`svc.protocol` walk (consumed-event-root collection) and
1360/// `emitter/lower.rs`'s own `cap_op_param_names` (`CommonsItem::Capability`/`c.ops`)
1361/// are exactly the P6.2/P6.6-class conversions this track's own §6 table still lists
1362/// as in scope, not body-rendering. Excluding either file would have hidden that real,
1363/// fixable surface from this probe the same way a path-prefix rule would — the harm
1364/// the named-not-prefixed discipline above exists to prevent, just at file granularity
1365/// instead of directory granularity.
1366const AST_IMPORTER_EXCEPTIONS: &[&str] = &["ir.rs", "ir/lower.rs", "project/tests_emit.rs"];
1367
1368/// Is `rel_path` (relative to `bynk-emit/src`) one of [`AST_IMPORTER_EXCEPTIONS`]?
1369fn is_named_ast_importer(rel_path: &Path) -> bool {
1370    let rel = rel_path.to_string_lossy().replace('\\', "/");
1371    AST_IMPORTER_EXCEPTIONS.contains(&rel.as_str())
1372}
1373
1374/// The files [`ast_importers`] counts: `bynk-emit/src` files whose contents match
1375/// `bynk_syntax::ast`, excluding [`AST_IMPORTER_EXCEPTIONS`]. Split out from
1376/// [`ast_importers`] so a test can assert on the actual survivor set, not just its
1377/// length (#1184 review).
1378fn ast_importer_files(root: &Path) -> Vec<PathBuf> {
1379    let dir = root.join("bynk-emit/src");
1380    rust_files(&dir)
1381        .into_iter()
1382        .filter(|(_, contents)| contents.contains("bynk_syntax::ast"))
1383        .filter(|(path, _)| !is_named_ast_importer(path.strip_prefix(&dir).unwrap_or(path)))
1384        .map(|(path, _)| path)
1385        .collect()
1386}
1387
1388/// R6.13. Files in `bynk-emit/src` that import `bynk_syntax::ast`, excluding
1389/// [`AST_IMPORTER_EXCEPTIONS`] — phase 6 (the AST import surface `bynk-emit` still
1390/// depends on directly). #1176: the unexcluded, crate-wide count could never reach 0
1391/// while `bynk-emit::ir`'s lowering pass exists at all; this exclusion is what lets the
1392/// probe track the track's real completion criterion (`the-ir.md` §5) instead of a
1393/// floor this track's own IR module structurally cannot clear.
1394fn ast_importers(root: &Path) -> Probe {
1395    Probe {
1396        name: "ast_importers",
1397        gated: true,
1398        reads: ast_importer_files(root).len().to_string(),
1399    }
1400}
1401
1402// --- Gated probe 9: emit_abi_shapes ---------------------------------------
1403
1404/// ADR 0310 D1's four emit-ABI shapes, as they surface as import names in the vendored
1405/// bindings — the `Result`/`Option` tag layout plus `JsonError`, `Uuid`, `FetchError`.
1406const EMIT_ABI: &[&str] = &[
1407    "Result",
1408    "Option",
1409    "Ok",
1410    "Err",
1411    "Some",
1412    "None",
1413    "JsonError",
1414    "Uuid",
1415    "FetchError",
1416];
1417
1418/// The capability interfaces a vendored binding legitimately imports to implement what
1419/// it declares — governed by language-stability rules, not ADR 0310's codegen-freeze
1420/// concern. See [`emit_abi_shapes`] and #999 Decision E for the two-list rationale.
1421const CAPABILITY_SURFACE: &[&str] = &[
1422    "Clock",
1423    "Fetch",
1424    "Idempotency",
1425    "Locale",
1426    "Logger",
1427    "Random",
1428    "Secrets",
1429    "Request",
1430    "Response",
1431    "LocaleTag",
1432    "Kv",
1433    "KVNamespace",
1434];
1435
1436/// Is `ident` one of ADR 0310's enumerated emit-ABI shapes, or part of the capability
1437/// surface a binding is required to import? If neither, it's a leak `emit_abi_shapes`
1438/// flags — this is the single predicate both the probe and its tests use, so a test
1439/// asserting "no leak" can't silently pass against a list the test itself redefined.
1440fn is_enumerated_emit_abi_or_capability_surface(ident: &str) -> bool {
1441    EMIT_ABI.contains(&ident) || CAPABILITY_SURFACE.contains(&ident)
1442}
1443
1444/// ADR 0310's probe (#999 Decision E). The vendored first-party bindings under
1445/// `bynk-check/src/firstparty/bindings/` must reference only [`EMIT_ABI`]'s nine names.
1446///
1447/// This does NOT count every non-enumerated import: a binding legitimately imports the
1448/// [`CAPABILITY_SURFACE`] interfaces it implements — that surface is governed by
1449/// language-stability rules, not ADR 0310's codegen-freeze concern, and a probe that
1450/// flagged it would read non-zero on every binding by construction. See #999 Decision
1451/// E for the two-list rationale and its falsifier.
1452fn emit_abi_shapes(root: &Path) -> Probe {
1453    let dir = root.join("bynk-check/src/firstparty/bindings");
1454    let mut leaks: Vec<String> = Vec::new();
1455    let Ok(entries) = std::fs::read_dir(&dir) else {
1456        return Probe {
1457            name: "emit_abi_shapes",
1458            gated: true,
1459            reads: "bindings directory not found".to_string(),
1460        };
1461    };
1462    let mut files: Vec<_> = entries.flatten().map(|e| e.path()).collect();
1463    files.sort();
1464    for path in files {
1465        if path.extension().is_none_or(|e| e != "ts") {
1466            continue;
1467        }
1468        let Ok(contents) = std::fs::read_to_string(&path) else {
1469            continue;
1470        };
1471        let name = path.file_name().unwrap().to_string_lossy().to_string();
1472        for ident in ts_named_imports_from_runtime_modules(&contents) {
1473            if !is_enumerated_emit_abi_or_capability_surface(&ident) {
1474                leaks.push(format!("{name}:{ident}"));
1475            }
1476        }
1477    }
1478    Probe {
1479        name: "emit_abi_shapes",
1480        gated: true,
1481        reads: format!("{} ({})", leaks.len(), leaks.join(", ")),
1482    }
1483}
1484
1485/// Named identifiers imported from the compiler-generated firstparty/runtime relative
1486/// modules (`./bynk.js`, `./runtime.js`, `./bynk/locale/types.js`, `./cloudflare.js`,
1487/// or their `../` forms) — `import type { A, B }`/`import { A, B }` braces, stripping
1488/// `type ` markers and `X as Y` aliases (keeping the imported name, not the local one,
1489/// since the allowlists are about what's referenced, not what it's called locally).
1490fn ts_named_imports_from_runtime_modules(src: &str) -> Vec<String> {
1491    let mut out = Vec::new();
1492    for line in src.lines() {
1493        let line = line.trim();
1494        if !line.starts_with("import") {
1495            continue;
1496        }
1497        let is_runtime_module = ["\"./bynk.js\"", "\"./runtime.js\"", "\"../runtime.js\""]
1498            .iter()
1499            .any(|m| line.ends_with(&format!("from {m};")))
1500            || line.contains("bynk/locale/types.js")
1501            || line.contains("cloudflare.js");
1502        if !is_runtime_module {
1503            continue;
1504        }
1505        let Some(open) = line.find('{') else { continue };
1506        let Some(close) = line.find('}') else {
1507            continue;
1508        };
1509        for part in line[open + 1..close].split(',') {
1510            let part = part.trim().trim_start_matches("type ").trim();
1511            if part.is_empty() {
1512                continue;
1513            }
1514            let imported = part.split(" as ").next().unwrap_or(part).trim();
1515            out.push(imported.to_string());
1516        }
1517    }
1518    out
1519}
1520
1521// --- Reported probe 1: wildcard_arms --------------------------------------
1522
1523/// R2.12. `clippy::wildcard_enum_match_arm` diagnostics, forced on via `-W` so the
1524/// count is real from day one and doesn't wait on `workspace_lints`/T0.3 (#999 Decision
1525/// C — delegating to clippy's own type-aware pass, rather than a hand-rolled scan for
1526/// "compiler-owned enum", so the probe and the enforcement mechanism can never
1527/// disagree). A count, not a boolean — moves on nearly every match statement anyone
1528/// writes, so it is reported, not gated (#999 Decision D).
1529fn wildcard_arms(root: &Path) -> Probe {
1530    let reads = match run_clippy_wildcard_scan(root) {
1531        Ok(n) => n.to_string(),
1532        Err(e) => format!("error running clippy: {e}"),
1533    };
1534    Probe {
1535        name: "wildcard_arms",
1536        gated: false,
1537        reads,
1538    }
1539}
1540
1541/// Runs clippy with the lint forced on and parses the NDJSON output properly —
1542/// **not** a substring count. A single `wildcard_enum_match_arm` diagnostic's JSON
1543/// repeats the lint name several times (the `code` field, the human-readable message,
1544/// the `#[warn(...)]` note, and the `rendered` field duplicating the whole thing as
1545/// text), so `stdout.matches("wildcard_enum_match_arm").count()` overcounts by roughly
1546/// 3x — caught by cross-checking this probe's own first run against a real JSON parse
1547/// (296 real diagnostics, not the naive scan's 888).
1548///
1549/// Checks the process exit status: a forced `-W` (not `-D`) never fails the build on
1550/// account of the lint itself, so a non-zero exit means clippy genuinely could not run
1551/// (a compile error elsewhere, a missing toolchain component, offline with no cached
1552/// index) — in which case stdout carries no `compiler-message` lines and a silent
1553/// success would report a false, and indistinguishable, `0`. This probe is reported,
1554/// not gated, precisely so an honest "couldn't measure" surfaces loudly here rather
1555/// than being read as "closed."
1556fn run_clippy_wildcard_scan(root: &Path) -> std::io::Result<usize> {
1557    let output = Command::new("cargo")
1558        .args([
1559            "clippy",
1560            "--workspace",
1561            "--message-format=json",
1562            "--",
1563            "-W",
1564            "clippy::wildcard_enum_match_arm",
1565        ])
1566        .current_dir(root)
1567        .output()?;
1568    if !output.status.success() {
1569        return Err(std::io::Error::other(format!(
1570            "cargo clippy exited with {}: {}",
1571            output.status,
1572            String::from_utf8_lossy(&output.stderr).trim()
1573        )));
1574    }
1575    let stdout = String::from_utf8_lossy(&output.stdout);
1576    let mut count = 0usize;
1577    for line in stdout.lines() {
1578        let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
1579            continue;
1580        };
1581        if value.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") {
1582            continue;
1583        }
1584        let code = value.pointer("/message/code/code").and_then(|c| c.as_str());
1585        if code == Some("clippy::wildcard_enum_match_arm") {
1586            count += 1;
1587        }
1588    }
1589    Ok(count)
1590}
1591
1592// --- Reported probe 2: keep_in_sync ---------------------------------------
1593
1594/// P2 (trend only). Comments across the workspace containing "in sync", "mirrors",
1595/// "parity", or "must match" — each one names a rule the compiler cannot teach itself
1596/// and must be taught in review, every time.
1597fn keep_in_sync(root: &Path) -> Probe {
1598    let phrases = ["in sync", "mirrors", "parity", "must match"];
1599    let mut count = 0usize;
1600    for dir in top_level_crate_dirs(root) {
1601        for (_, contents) in rust_files(&dir.join("src")) {
1602            for line in contents.lines() {
1603                if is_line_comment(line) {
1604                    let lower = line.to_lowercase();
1605                    if phrases.iter().any(|p| lower.contains(p)) {
1606                        count += 1;
1607                    }
1608                }
1609            }
1610        }
1611    }
1612    Probe {
1613        name: "keep_in_sync",
1614        gated: false,
1615        reads: count.to_string(),
1616    }
1617}
1618
1619// --- Reported probe 3: test_density ---------------------------------------
1620
1621/// R11.1, and §3.4's phase-3 trigger. Per crate: (lines inside `#[test]` fn bodies,
1622/// plus lines inside `#[cfg(test)] mod` blocks outside those fns) ÷ (non-blank,
1623/// non-comment lines under that crate's `src/`) — #999 Decision F's definition,
1624/// written down precisely because an undefined "ratio" is exactly the ambiguity that
1625/// produced the track doc §9's four-row ambiguity.
1626fn test_density(root: &Path) -> Probe {
1627    let mut parts = Vec::new();
1628    for dir in top_level_crate_dirs(root) {
1629        let name = dir.file_name().unwrap().to_string_lossy().to_string();
1630        let src_dir = dir.join("src");
1631        let mut test_lines = 0usize;
1632        let mut code_lines = 0usize;
1633        for (_, contents) in rust_files(&src_dir) {
1634            let lines: Vec<&str> = contents.lines().collect();
1635            let ranges = test_mod_ranges(&lines);
1636            for (i, line) in lines.iter().enumerate() {
1637                let is_blank_or_comment = line.trim().is_empty() || is_line_comment(line);
1638                if !is_blank_or_comment {
1639                    code_lines += 1;
1640                }
1641                if in_test_range(i, &ranges) && !is_blank_or_comment {
1642                    test_lines += 1;
1643                }
1644            }
1645        }
1646        if code_lines > 0 {
1647            let ratio = 100.0 * test_lines as f64 / code_lines as f64;
1648            parts.push(format!("{name}={ratio:.1}%"));
1649        }
1650    }
1651    Probe {
1652        name: "test_density",
1653        gated: false,
1654        reads: parts.join(", "),
1655    }
1656}
1657
1658// --- Reported probe 4: fixture_kinds --------------------------------------
1659
1660/// R11.2. Fixture directories under `bynkc/tests` using each assertion granularity —
1661/// `expected_contains.txt` / `expected_absent.txt` / `expected_diagnostics.txt` — set
1662/// against the older, coarser `expected_error.txt` (category-string) convention.
1663fn fixture_kinds(root: &Path) -> Probe {
1664    let tests_dir = root.join("bynkc/tests");
1665    let contains = count_files_named(&tests_dir, "expected_contains.txt");
1666    let absent = count_files_named(&tests_dir, "expected_absent.txt");
1667    let diagnostics = count_files_named(&tests_dir, "expected_diagnostics.txt");
1668    let error = count_files_named(&tests_dir, "expected_error.txt");
1669    Probe {
1670        name: "fixture_kinds",
1671        gated: false,
1672        reads: format!(
1673            "contains={contains}, absent={absent}, diagnostics={diagnostics}, error={error}"
1674        ),
1675    }
1676}
1677
1678fn count_files_named(dir: &Path, filename: &str) -> usize {
1679    let mut count = 0usize;
1680    count_files_named_walk(dir, filename, &mut count);
1681    count
1682}
1683
1684fn count_files_named_walk(dir: &Path, filename: &str, count: &mut usize) {
1685    let Ok(entries) = std::fs::read_dir(dir) else {
1686        return;
1687    };
1688    for entry in entries.flatten() {
1689        let path = entry.path();
1690        if path.is_dir() {
1691            count_files_named_walk(&path, filename, count);
1692        } else if path.file_name().is_some_and(|n| n == filename) {
1693            *count += 1;
1694        }
1695    }
1696}
1697
1698// --- Rendering + diffing ---------------------------------------------------
1699
1700/// The committed table: a plain Markdown table, probe name → gated?/reads, plus a
1701/// pointer to the rule ledger `stamp::apply` writes (#1001).
1702pub fn render_table(report: &Report) -> String {
1703    let mut out = String::new();
1704    out.push_str("<!-- GENERATED FILE — do not edit by hand.\n");
1705    out.push_str("     Source: cargo xtask greenfield-status (xtask/src/greenfield_status.rs).\n");
1706    out.push_str("     Regenerate with: cargo xtask greenfield-status --apply -->\n\n");
1707    out.push_str("# Greenfield status\n\n");
1708    out.push_str(
1709        "Track slice T0.0 (#999). Nine probes are gated — a disagreement between this \
1710         file and a fresh run fails `greenfield_status_table_is_current` \
1711         (`xtask/tests/greenfield_status.rs`). Four are trend probes, reported only.\n\n",
1712    );
1713    out.push_str("| Probe | Gated | Reads |\n|---|---|---|\n");
1714    for probe in &report.probes {
1715        let _ = writeln!(
1716            out,
1717            "| `{}` | {} | {} |",
1718            probe.name,
1719            if probe.gated { "yes" } else { "no (trend)" },
1720            probe.reads
1721        );
1722    }
1723
1724    out.push_str("\n## Rules closed\n\n");
1725    // A static, unconditional link — not a count, and not even an existence
1726    // check. A first draft read `design/greenfield-status-rules.md` here to
1727    // report a row count, but nothing regenerates *this* file when `stamp`
1728    // writes the ledger (`stamp.yml` never runs `greenfield-status --apply`,
1729    // and the gating test only diffs the nine probes) — so a count or an
1730    // exists/doesn't-exist message would silently go stale the moment the
1731    // first `closes_rule` landed, which is exactly the drift this section
1732    // exists to avoid, not invite (#1001 review). Static text can't go stale;
1733    // the ledger is one click away either way.
1734    out.push_str(
1735        "See [`design/greenfield-status-rules.md`](greenfield-status-rules.md) for rule ids \
1736         closed so far (written by `cargo xtask stamp --apply` at merge; may not exist yet if \
1737         no increment has cited `closes_rule`).\n",
1738    );
1739    out
1740}
1741
1742/// Every gated probe whose live reading disagrees with the committed table's, as
1743/// `(probe name, committed, live)`. Trend probes are never compared, and never
1744/// computed here — this only runs the nine gated probes, so checking currency never
1745/// pays for `wildcard_arms`'s workspace-wide clippy pass. For a caller that has already
1746/// run the full report (e.g. to print it), use [`gated_disagreements_in`] instead so the
1747/// nine gated probes aren't computed a second time.
1748pub fn gated_disagreements(root: &Path) -> Vec<(String, String, String)> {
1749    gated_disagreements_in(&run_gated(root), root)
1750}
1751
1752/// Like [`gated_disagreements`], but diffs `probes` (typically a [`Report`]'s
1753/// `.probes`, already computed) instead of re-running the gated probes.
1754pub fn gated_disagreements_in(probes: &[Probe], root: &Path) -> Vec<(String, String, String)> {
1755    let committed = std::fs::read_to_string(table_path(root)).unwrap_or_default();
1756    let mut out = Vec::new();
1757    for probe in probes.iter().filter(|p| p.gated) {
1758        let row_prefix = format!("| `{}` | yes | ", probe.name);
1759        let committed_reads = committed
1760            .lines()
1761            .find(|l| l.starts_with(&row_prefix))
1762            .and_then(|l| l.strip_prefix(&row_prefix))
1763            .and_then(|l| l.strip_suffix(" |"))
1764            .unwrap_or("<row missing>");
1765        if committed_reads != probe.reads {
1766            out.push((
1767                probe.name.to_string(),
1768                committed_reads.to_string(),
1769                probe.reads.clone(),
1770            ));
1771        }
1772    }
1773    out
1774}
1775
1776#[cfg(test)]
1777mod tests {
1778    use super::*;
1779
1780    // --- emit_diagnostics (#999 Decision A) ---------------------------------
1781
1782    /// A standalone `"bynk.foo"` literal is found — the ordinary case.
1783    #[test]
1784    fn bynk_dotted_literals_finds_standalone_literal() {
1785        let src = r#"code("bynk.check.something", "a message")"#;
1786        assert_eq!(bynk_dotted_literals(src), vec!["bynk.check.something"]);
1787    }
1788
1789    /// The bug this slice found in its own first draft: a longer message that merely
1790    /// *starts* with "bynk." must not be truncated into a fake code literal. Regression
1791    /// test for `bynk.map itself uses bynk.list, so list must be injected too: {paths:?}`
1792    /// (`bynk-emit/src/project.rs`), which an earlier, less careful version of this scan
1793    /// wrongly counted as the literal `"bynk.map"`.
1794    #[test]
1795    fn bynk_dotted_literals_ignores_prefix_of_a_longer_message() {
1796        let src = r#"assert!(cond, "bynk.map itself uses bynk.list, so list must be injected too: {paths:?}");"#;
1797        assert!(bynk_dotted_literals(src).is_empty());
1798    }
1799
1800    /// Regression test for the other half of the same bug: a `\`-continued string
1801    /// literal (`"bynk.emit.unresolved_cross_context_signature: no signature for \`,
1802    /// continued on the next source line) is one string, not a diagnostic-code literal,
1803    /// even though its first segment matches the identifier charset — because the
1804    /// character after the run is `:`, never a closing quote, on either line.
1805    #[test]
1806    fn bynk_dotted_literals_ignores_a_line_continued_message() {
1807        let src =
1808            "\"bynk.emit.unresolved_cross_context_signature: no signature for \\\n     the rest\"";
1809        assert!(bynk_dotted_literals(src).is_empty());
1810    }
1811
1812    /// The whole point of Decision A: cross-referencing the real registry, not a
1813    /// hand-maintained exclusion list, correctly separates a real diagnostic code from
1814    /// a commons/namespace path that merely looks like one.
1815    #[test]
1816    fn emit_diagnostics_cross_references_the_real_registry() {
1817        let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
1818            .iter()
1819            .map(|d| d.code)
1820            .collect();
1821        // A code this registry is known to carry (bynk-syntax/src/diagnostics.rs).
1822        assert!(registry.contains("bynk.parse.expected_expression"));
1823        // A commons/namespace path, not a diagnostic code — #999's own verified survey.
1824        assert!(!registry.contains("bynk.locale"));
1825    }
1826
1827    // --- ast_importers (#1176) ------------------------------------------------
1828
1829    /// The exclusion is named, not prefixed: `ir.rs`/`ir/lower.rs` are the lowering
1830    /// pass's own legitimate `Ast → Ir` import; `project/tests_emit.rs` is the
1831    /// Q7-settled (`the-ir.md` §3.7) `Ir → String` half that keeps hand-writing
1832    /// TypeScript by calling straight into `emitter.rs`'s own body-rendering, and
1833    /// keeps reading a handler's declared param/return `TypeRef` with no `TyId`
1834    /// available at that call site — but `project.rs` (which also imports
1835    /// `bynk_syntax::ast`, via `EmitProjectCtx`) must stay counted, and so, per
1836    /// review of #1210, must `emitter.rs`/`emitter/lower.rs` themselves: both still
1837    /// hold live AST-*declaration* reads (`emitter.rs`'s `CommonsItem::Service`/
1838    /// `svc.protocol` walk, `emitter/lower.rs`'s `cap_op_param_names`) that are the
1839    /// still-open R6.13 defect this probe tracks, not the Q7 kind — excluding either
1840    /// file would hide that real work the same way a path-prefix rule would. A
1841    /// path-prefix rule (e.g. "only `emitter/**` counts") would have excluded
1842    /// `project.rs` right along with the legitimate three, silently undercounting
1843    /// real work.
1844    #[test]
1845    fn ast_importer_exclusion_is_named_not_prefixed() {
1846        assert!(is_named_ast_importer(Path::new("ir.rs")));
1847        assert!(is_named_ast_importer(Path::new("ir/lower.rs")));
1848        assert!(is_named_ast_importer(Path::new("project/tests_emit.rs")));
1849        assert!(!is_named_ast_importer(Path::new("project.rs")));
1850        assert!(!is_named_ast_importer(Path::new("emitter.rs")));
1851        assert!(!is_named_ast_importer(Path::new("emitter/lower.rs")));
1852        assert!(!is_named_ast_importer(Path::new("emitter/workers.rs")));
1853        assert!(!is_named_ast_importer(Path::new("ir/other.rs")));
1854    }
1855
1856    /// #1184 review: an `AST_IMPORTER_EXCEPTIONS` entry going stale (renamed or split,
1857    /// e.g. `ir/lower.rs` becoming `ir/lower/mod.rs`) must fail loud here, not surface
1858    /// as a silent `ast_importers` regression in `greenfield_status_table_is_current` —
1859    /// mirrors [`file_is_named_fs_floor`]'s own "fail loud, not quiet" discipline.
1860    #[test]
1861    fn ast_importer_exceptions_still_exist_and_still_import_the_ast() {
1862        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1863            .join("..")
1864            .join("bynk-emit/src");
1865        for rel in AST_IMPORTER_EXCEPTIONS {
1866            let contents = std::fs::read_to_string(dir.join(rel)).unwrap_or_else(|e| {
1867                panic!("AST_IMPORTER_EXCEPTIONS entry {rel:?} does not exist: {e}")
1868            });
1869            assert!(
1870                contents.contains("bynk_syntax::ast"),
1871                "AST_IMPORTER_EXCEPTIONS entry {rel:?} no longer imports bynk_syntax::ast \
1872                 — it excludes nothing and should be removed"
1873            );
1874        }
1875    }
1876
1877    /// #1184 review, extended by #1187's own closing scoping pass (and narrowed by
1878    /// review of #1210, which found `emitter.rs`/`emitter/lower.rs` still hold live,
1879    /// in-scope AST-declaration reads and must stay counted): exercises the real
1880    /// filter over the live tree, not just the pure predicate — the survivor set the
1881    /// PR's own named-vs-prefix argument depends on: the three named exclusions drop
1882    /// out (`ir/`'s legitimate `Ast → Ir` pair, plus `project/tests_emit.rs`'s
1883    /// Q7-settled `Ir → String` case), `project.rs` and `emitter/workers.rs` (R6.13's
1884    /// still-open AST-declaration reads, the latter standing in for
1885    /// `emitter.rs`/`emitter/lower.rs` themselves) do not.
1886    #[test]
1887    fn ast_importers_excludes_the_named_pairs_but_counts_project_rs() {
1888        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
1889        let dir = root.join("bynk-emit/src");
1890        let counted: BTreeSet<String> = ast_importer_files(&root)
1891            .into_iter()
1892            .map(|path| {
1893                path.strip_prefix(&dir)
1894                    .unwrap_or(&path)
1895                    .to_string_lossy()
1896                    .replace('\\', "/")
1897            })
1898            .collect();
1899        assert!(!counted.contains("ir.rs"));
1900        assert!(!counted.contains("ir/lower.rs"));
1901        assert!(!counted.contains("project/tests_emit.rs"));
1902        assert!(counted.contains("project.rs"));
1903        assert!(counted.contains("emitter.rs"));
1904        assert!(counted.contains("emitter/lower.rs"));
1905        assert!(counted.contains("emitter/workers.rs"));
1906    }
1907
1908    // --- fs_below_driver / test_density (trailing `#[cfg(test)] mod tests {}`) ---
1909
1910    #[test]
1911    fn production_std_fs_usage_is_detected() {
1912        let src = "fn load(p: &Path) -> String {\n    std::fs::read_to_string(p).unwrap()\n}\n";
1913        assert!(has_production_std_fs(src));
1914    }
1915
1916    #[test]
1917    fn std_fs_inside_a_trailing_test_mod_is_not_production() {
1918        let src = "fn load(p: &Path) -> String {\n    String::new()\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn t() {\n        std::fs::write(\"x\", \"y\").unwrap();\n    }\n}\n";
1919        assert!(!has_production_std_fs(src));
1920    }
1921
1922    /// Regression test for the other real bug this slice found: `bynk-emit/src/lib.rs`
1923    /// has `#[cfg(test)] pub(crate) mod testkit;` — an external-file module
1924    /// *declaration* (ends in `;`), not an inline block. It must not be mistaken for a
1925    /// scope-opening `mod tests { ... }`, or the (genuinely production) code after it in
1926    /// the same file would be wrongly excluded.
1927    #[test]
1928    fn cfg_test_external_mod_declaration_does_not_open_a_test_region() {
1929        let src = "#[cfg(test)]\npub(crate) mod testkit;\n\nfn load(p: &Path) -> String {\n    std::fs::read_to_string(p).unwrap()\n}\n";
1930        assert!(has_production_std_fs(src));
1931    }
1932
1933    /// Regression test for the bug caught in review: a file with **two** scattered
1934    /// `#[cfg(test)] mod ... { ... }` blocks, with real production code between them —
1935    /// exactly `bynk-emit/src/emitter/lower.rs`'s shape (two test modules, 1031
1936    /// production lines apart). A single "everything from the first/last `#[cfg(test)]`
1937    /// onward" cutoff would misclassify `lower_lambda` here as test-scope; the fix must
1938    /// close each block at its own boundary and resume production scanning after it.
1939    #[test]
1940    fn production_code_between_two_scattered_test_mods_is_detected() {
1941        let src = "\
1942#[cfg(test)]
1943mod decode_map_key_tests {
1944    #[test]
1945    fn t() {
1946        assert_eq!(1, 1);
1947    }
1948}
1949
1950fn lower_lambda(p: &Path) -> String {
1951    std::fs::read_to_string(p).unwrap()
1952}
1953
1954#[cfg(test)]
1955mod idempotency_scoping_tests {
1956    #[test]
1957    fn t2() {
1958        assert_eq!(2, 2);
1959    }
1960}
1961";
1962        assert!(has_production_std_fs(src));
1963    }
1964
1965    /// The same fixture's `test_mod_ranges` shape, checked directly: two disjoint
1966    /// ranges, not one span from the first block to the last.
1967    #[test]
1968    fn test_mod_ranges_finds_each_block_separately() {
1969        let src = "\
1970#[cfg(test)]
1971mod a {
1972    fn x() {}
1973}
1974
1975fn production() {}
1976
1977#[cfg(test)]
1978mod b {
1979    fn y() {}
1980}
1981";
1982        let lines: Vec<&str> = src.lines().collect();
1983        let ranges = test_mod_ranges(&lines);
1984        assert_eq!(
1985            ranges.len(),
1986            2,
1987            "expected two disjoint test-mod ranges: {ranges:?}"
1988        );
1989        // Line 5 (0-indexed) is `fn production() {}`, between the two blocks.
1990        assert!(
1991            !in_test_range(5, &ranges),
1992            "production() must not read as test-scope"
1993        );
1994    }
1995
1996    /// Regression test for the bug in the *fix* for the above: a column-0-`}`
1997    /// shortcut (tried and reverted during review) truncates a test module the moment
1998    /// its body embeds a multi-line fixture string containing a `}` flush against the
1999    /// left margin — exactly `bynk-ide/src/sequence.rs`'s shape, whose test mod embeds
2000    /// `.bynk` source fixtures. The real brace-depth scanner must see through the
2001    /// string and find the module's *actual* closing brace, hundreds of lines later.
2002    /// Uses a raw string for the outer fixture so the embedded `"..."` doesn't need
2003    /// escaping, and locates the real end by content rather than a hand-counted index
2004    /// — a hand-counted line number is exactly the kind of easy-to-miscount detail
2005    /// this codebase's own convention (verify, don't assume) warns against.
2006    #[test]
2007    fn test_mod_ranges_is_not_fooled_by_a_column_zero_brace_inside_a_string() {
2008        let src = r#"#[cfg(test)]
2009mod tests {
2010    const FIXTURE: &str = "
2011commons app.demo {
2012}
2013";
2014
2015    fn real_end_of_module() {}
2016}
2017"#;
2018        let lines: Vec<&str> = src.lines().collect();
2019        let ranges = test_mod_ranges(&lines);
2020        assert_eq!(ranges.len(), 1, "expected exactly one range: {ranges:?}");
2021        let (_, end) = ranges[0];
2022        // `str::lines()` drops the trailing newline, so the module's real closing
2023        // brace — the fixture's last line — is at `lines.len() - 1`. The string's
2024        // embedded `}` (an earlier line) must not be mistaken for it.
2025        assert_eq!(
2026            end,
2027            lines.len() - 1,
2028            "closed too early — mistook the string's `}}` for the module's: {ranges:?}"
2029        );
2030    }
2031
2032    // --- fs_below_driver: import resolution through `use super::*;` (#1013) ---
2033
2034    /// Run [`production_std_fs_files`] over an in-memory crate layout and name the
2035    /// flagged files, so each case reads as "these files, and only these".
2036    fn flagged(files: &[(&str, &str)]) -> Vec<String> {
2037        let owned: Vec<(PathBuf, String)> = files
2038            .iter()
2039            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
2040            .collect();
2041        production_std_fs_files(&owned)
2042            .into_iter()
2043            .map(|i| files[i].0.to_string())
2044            .collect()
2045    }
2046
2047    /// The concrete #1013 instance, in miniature: `project.rs` has a module-level
2048    /// `use std::fs;` (ancestor-scoped, so visible to descendants), `discovery.rs`
2049    /// glob-imports it via `use super::*;` and calls bare `fs::read_to_string` —
2050    /// touching `std::fs` in production while never spelling it. The text scan alone
2051    /// reads only `project.rs`; the resolved probe must read both.
2052    #[test]
2053    fn bare_fs_reached_through_a_glob_imported_parent_is_flagged() {
2054        let files = [
2055            ("lib.rs", "mod project;\n"),
2056            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2057            (
2058                "project/discovery.rs",
2059                "use super::*;\n\nfn read_source(path: &std::path::Path) -> String {\n    fs::read_to_string(path).unwrap()\n}\n",
2060            ),
2061        ];
2062        assert!(
2063            !has_production_std_fs(files[2].1),
2064            "the text scan alone must miss it"
2065        );
2066        assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
2067    }
2068
2069    /// Without `use super::*;` there is no path from the bare `fs::` to the parent's
2070    /// binding — the probe must not guess one into existence.
2071    #[test]
2072    fn bare_fs_without_a_glob_super_import_is_not_flagged() {
2073        let files = [
2074            ("lib.rs", "mod project;\n"),
2075            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2076            (
2077                "project/discovery.rs",
2078                "fn read_source(path: &std::path::Path) -> String {\n    fs::read_to_string(path).unwrap()\n}\n",
2079            ),
2080        ];
2081        assert_eq!(flagged(&files), vec!["project.rs"]);
2082    }
2083
2084    /// Glob chains re-reach ancestors transitively — grandparent binds `fs`, both
2085    /// hops glob-import `super::*` — and the `mod.rs` layout maps to the same module
2086    /// tree as the `name.rs` one. The middle file sees `fs` but never uses it, so
2087    /// only the leaf joins the (text-flagged) root.
2088    #[test]
2089    fn glob_super_resolution_is_transitive_across_mod_rs_parents() {
2090        let files = [
2091            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2092            ("a/mod.rs", "use super::*;\n\nmod b;\n"),
2093            (
2094                "a/b.rs",
2095                "use super::*;\n\nfn walk() {\n    let _ = fs::read_dir(\".\");\n}\n",
2096            ),
2097        ];
2098        assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
2099    }
2100
2101    /// A break anywhere in the chain stops resolution: the middle module does not
2102    /// glob-import `super::*`, so the leaf's `use super::*;` reaches a module with no
2103    /// `fs` binding to offer.
2104    #[test]
2105    fn a_break_in_the_glob_chain_stops_resolution() {
2106        let files = [
2107            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2108            ("a/mod.rs", "mod b;\n"),
2109            (
2110                "a/b.rs",
2111                "use super::*;\n\nfn walk() {\n    let _ = fs::read_dir(\".\");\n}\n",
2112            ),
2113        ];
2114        assert_eq!(flagged(&files), vec!["lib.rs"]);
2115    }
2116
2117    /// Nearest binding wins, as in Rust: the child re-binds `fs` to something that is
2118    /// not `std::fs`, so its bare `fs::` calls are that something's, not std's.
2119    #[test]
2120    fn a_local_non_std_binding_shadows_the_ancestors_std_fs() {
2121        let files = [
2122            ("lib.rs", "mod project;\n"),
2123            ("project.rs", "use std::fs;\n\nmod overlay;\nmod d;\n"),
2124            ("project/overlay.rs", "pub fn read(_p: &str) {}\n"),
2125            (
2126                "project/d.rs",
2127                "use super::*;\nuse crate::project::overlay as fs;\n\nfn f() {\n    let _ = fs::read(\"x\");\n}\n",
2128            ),
2129        ];
2130        assert_eq!(flagged(&files), vec!["project.rs"]);
2131    }
2132
2133    /// An aliased module binding resolves under its alias — the call site never
2134    /// contains the substring `fs::` at all.
2135    #[test]
2136    fn an_aliased_std_fs_binding_resolves_through_the_glob() {
2137        let files = [
2138            ("lib.rs", "mod p;\n"),
2139            ("p.rs", "use std::fs as stdfs;\n\nmod c;\n"),
2140            (
2141                "p/c.rs",
2142                "use super::*;\n\nfn f() {\n    stdfs::write(\"a\", \"b\").unwrap();\n}\n",
2143            ),
2144        ];
2145        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2146    }
2147
2148    /// `use std::{fs, io};` binds `fs` without ever containing the substring
2149    /// `std::fs` — the same blind spot as #1013's, one file deep. Resolution applies
2150    /// in the file's own scope, no glob import required.
2151    #[test]
2152    fn a_group_imported_fs_binding_is_resolved_in_its_own_file() {
2153        let src = "use std::{fs, io};\n\nfn f() -> io::Result<()> {\n    fs::metadata(\"x\").map(|_| ())\n}\n";
2154        assert!(
2155            !has_production_std_fs(src),
2156            "the text scan alone must miss it"
2157        );
2158        let files = [("thing.rs", src)];
2159        assert_eq!(flagged(&files), vec!["thing.rs"]);
2160    }
2161
2162    /// The item-import shape #1013 scope-checked (zero current instances), at the
2163    /// granularity this probe can reach: an ancestor's `use std::fs::File;` used as a
2164    /// bare path root `File::open` in a glob-importing child resolves and flags. (A
2165    /// bare *call* of an imported fn — `read_to_string(p)`, no `::` — presents no
2166    /// path root and remains out of a text-level scanner's reach, per the doc.)
2167    #[test]
2168    fn an_item_import_under_std_fs_resolves_as_a_path_root() {
2169        let files = [
2170            ("lib.rs", "mod p;\n"),
2171            ("p.rs", "use std::fs::File;\n\nmod c;\n"),
2172            (
2173                "p/c.rs",
2174                "use super::*;\n\nfn f() {\n    let _ = File::open(\"x\");\n}\n",
2175            ),
2176        ];
2177        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2178    }
2179
2180    /// A test module's `use super::*;` and tempdir `fs::` calls are test-scope — the
2181    /// `bynk-ide` files' shape (`architecture.rs`, `sequence.rs`), which must stay
2182    /// unflagged exactly as they were under the text-only scan.
2183    #[test]
2184    fn glob_and_bare_fs_inside_a_test_mod_stay_test_scope() {
2185        let files = [
2186            ("lib.rs", "use std::fs;\n\nmod w;\n"),
2187            (
2188                "w.rs",
2189                "fn production() {}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::fs;\n\n    #[test]\n    fn t() {\n        let _ = fs::read_dir(\".\");\n    }\n}\n",
2190            ),
2191        ];
2192        assert_eq!(flagged(&files), vec!["lib.rs"]);
2193    }
2194
2195    /// The module-tree mapping behind the resolution, checked directly: `name.rs` and
2196    /// `mod.rs` layouts, a preferred `a.rs` over `a/mod.rs`, and rootless roots.
2197    #[test]
2198    fn module_parent_maps_both_file_layouts() {
2199        let files: Vec<(PathBuf, String)> = ["lib.rs", "a.rs", "a/b.rs", "c/mod.rs", "c/d.rs"]
2200            .iter()
2201            .map(|p| (PathBuf::from(p), String::new()))
2202            .collect();
2203        let idx = |name: &str| {
2204            files
2205                .iter()
2206                .position(|(p, _)| p == Path::new(name))
2207                .unwrap()
2208        };
2209        assert_eq!(module_parent(Path::new("lib.rs"), &files), None);
2210        assert_eq!(
2211            module_parent(Path::new("a.rs"), &files),
2212            Some(idx("lib.rs"))
2213        );
2214        assert_eq!(
2215            module_parent(Path::new("a/b.rs"), &files),
2216            Some(idx("a.rs"))
2217        );
2218        assert_eq!(
2219            module_parent(Path::new("c/mod.rs"), &files),
2220            Some(idx("lib.rs"))
2221        );
2222        assert_eq!(
2223            module_parent(Path::new("c/d.rs"), &files),
2224            Some(idx("c/mod.rs"))
2225        );
2226    }
2227
2228    // --- fs_below_driver: #1016 review findings ------------------------------
2229
2230    /// Finding 1: a `super::`-qualified path needs no glob import — module privacy is
2231    /// ancestor-scoped, so `super::fs` names the parent's private `use std::fs;` from
2232    /// any child. One disambiguating edit away from `discovery.rs:39`'s bare call,
2233    /// and it must not drop the file out of the count.
2234    #[test]
2235    fn a_super_qualified_path_resolves_without_a_glob_import() {
2236        let files = [
2237            ("lib.rs", "mod project;\n"),
2238            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2239            (
2240                "project/discovery.rs",
2241                "fn read_source(path: &std::path::Path) -> String {\n    super::fs::read_to_string(path).unwrap()\n}\n",
2242            ),
2243        ];
2244        assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
2245    }
2246
2247    /// Finding 1, the `crate::`-rooted form: the walk descends the module tree from
2248    /// the crate root file by file, then resolves the leaf against that module's
2249    /// bindings — from anywhere in the crate, glob import or not.
2250    #[test]
2251    fn a_crate_qualified_path_resolves_through_the_module_tree() {
2252        let files = [
2253            ("lib.rs", "mod other;\nmod project;\n"),
2254            (
2255                "other.rs",
2256                "fn f() {\n    let _ = crate::project::fs::read_dir(\".\");\n}\n",
2257            ),
2258            ("project.rs", "use std::fs;\n"),
2259        ];
2260        assert_eq!(flagged(&files), vec!["other.rs", "project.rs"]);
2261    }
2262
2263    /// Finding 1, stacked hops: `super::super::` climbs two parents (through a
2264    /// glob-free middle module — qualified paths don't need the glob chain).
2265    #[test]
2266    fn stacked_super_hops_climb_the_parent_chain() {
2267        let files = [
2268            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2269            ("a/mod.rs", "mod b;\n"),
2270            (
2271                "a/b.rs",
2272                "fn f() {\n    let _ = super::super::fs::read_dir(\".\");\n}\n",
2273            ),
2274        ];
2275        assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
2276    }
2277
2278    /// Finding 1, `self::` composed with the glob chain: `self::fs` resolves in the
2279    /// file's own namespace, which includes what its `use super::*;` pulled in.
2280    #[test]
2281    fn a_self_qualified_path_resolves_through_the_files_own_glob_chain() {
2282        let files = [
2283            ("lib.rs", "mod p;\n"),
2284            ("p.rs", "use std::fs;\n\nmod c;\n"),
2285            (
2286                "p/c.rs",
2287                "use super::*;\n\nfn f() {\n    let _ = self::fs::read_dir(\".\");\n}\n",
2288            ),
2289        ];
2290        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2291    }
2292
2293    /// Finding 1's negatives: a qualified path to a name the parent binds to
2294    /// something other than `std::fs` stops at that binding, and a path through a
2295    /// module that doesn't exist resolves nowhere.
2296    #[test]
2297    fn a_qualified_path_to_a_non_std_binding_or_missing_module_is_not_flagged() {
2298        let files = [
2299            ("lib.rs", "mod overlay;\nmod p;\n"),
2300            ("overlay.rs", "pub fn read_dir(_p: &str) {}\n"),
2301            ("p.rs", "use crate::overlay as fs;\n\nmod d;\n"),
2302            (
2303                "p/d.rs",
2304                "fn f() {\n    let _ = super::fs::read_dir(\".\");\n    let _ = crate::missing::fs::read_dir(\".\");\n}\n",
2305            ),
2306        ];
2307        assert_eq!(flagged(&files), Vec::<String>::new());
2308    }
2309
2310    /// Finding 2: a locally-declared type-namespace item beats a glob-imported name
2311    /// in real Rust — a child with its own `mod fs;` calling `fs::…` is calling its
2312    /// own submodule, not the ancestor's `std::fs`.
2313    #[test]
2314    fn a_locally_declared_module_shadows_the_ancestors_std_fs() {
2315        let files = [
2316            ("lib.rs", "mod p;\n"),
2317            ("p.rs", "use std::fs;\n\nmod c;\n"),
2318            (
2319                "p/c.rs",
2320                "use super::*;\n\nmod fs;\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2321            ),
2322            ("p/c/fs.rs", "pub fn read_dir(_p: &str) {}\n"),
2323        ];
2324        assert_eq!(flagged(&files), vec!["p.rs"]);
2325    }
2326
2327    /// Finding 3: a trailing `//` comment on a `use` line must not sever the edge —
2328    /// neither the glob (`use super::*; // …`) nor the binding (`use std::fs; // …`).
2329    #[test]
2330    fn a_trailing_comment_on_a_use_line_does_not_sever_resolution() {
2331        let files = [
2332            ("lib.rs", "mod p;\n"),
2333            (
2334                "p.rs",
2335                "use std::fs; // read_source's disk fallback\n\nmod c;\n",
2336            ),
2337            (
2338                "p/c.rs",
2339                "use super::*; // parent's fs, PathBuf\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2340            ),
2341        ];
2342        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2343    }
2344
2345    /// Finding 4: the nested-group + `::self` normalisation branches, pinned
2346    /// directly — `use std::{fs::{self, File}, io};` binds `fs` *and* `File` to
2347    /// `std::fs`, and `io` only to the shadow set. Getting `::self` wrong would
2348    /// silently under-count, which is exactly this probe's failure mode.
2349    #[test]
2350    fn a_nested_group_with_self_binds_the_module_and_its_items() {
2351        let facts = fs_import_facts("use std::{fs::{self, File}, io};\n");
2352        let bound: Vec<&str> = facts.std_fs_bindings.iter().map(String::as_str).collect();
2353        assert_eq!(bound, vec!["File", "fs"]);
2354        assert!(facts.use_bound_names.contains("io"));
2355        assert!(!facts.std_fs_bindings.contains("io"));
2356    }
2357
2358    /// Finding 4, the children half of [`FsImportFacts`]' contract: a parent whose
2359    /// *only* `use std::fs;` lives in its `#[cfg(test)] mod` hands no binding to a
2360    /// glob-importing child — `bynk-ide/src/symbols.rs`' shape, latent until it
2361    /// grows a submodule.
2362    #[test]
2363    fn a_parents_test_mod_use_std_fs_does_not_reach_its_children() {
2364        let files = [
2365            ("lib.rs", "mod p;\n"),
2366            (
2367                "p.rs",
2368                "mod c;\n\nfn production() {}\n\n#[cfg(test)]\nmod tests {\n    use std::fs;\n\n    #[test]\n    fn t() {\n        let _ = fs::read_dir(\".\");\n    }\n}\n",
2369            ),
2370            (
2371                "p/c.rs",
2372                "use super::*;\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2373            ),
2374        ];
2375        assert_eq!(flagged(&files), Vec::<String>::new());
2376    }
2377
2378    // --- fs_below_driver: named-floor classification (#1104) -----------------
2379
2380    #[test]
2381    fn fn_name_on_line_strips_modifiers() {
2382        assert_eq!(fn_name_on_line("fn foo() {"), Some("foo".to_string()));
2383        assert_eq!(
2384            fn_name_on_line("pub(crate) fn read_adapter_binding("),
2385            Some("read_adapter_binding".to_string())
2386        );
2387        assert_eq!(
2388            fn_name_on_line("pub async unsafe fn go() {"),
2389            Some("go".to_string())
2390        );
2391    }
2392
2393    #[test]
2394    fn fn_name_on_line_ignores_non_fn_lines() {
2395        assert_eq!(fn_name_on_line("    let f = foo();"), None);
2396        assert_eq!(fn_name_on_line("/// calls fn bar somewhere"), None);
2397    }
2398
2399    /// A signature whose `{` arrives lines after the `fn` line — `read_adapter_binding`'s
2400    /// own real shape — must still resolve to the correct body range: `started` can't
2401    /// flip true on the parameter list, which has no braces of its own.
2402    #[test]
2403    fn production_fn_ranges_handles_a_wrapped_signature() {
2404        let src = "pub(crate) fn read_adapter_binding(\n    path: &Path,\n) -> std::io::Result<String> {\n    fs::read_to_string(path)\n}\n";
2405        let lines: Vec<&str> = src.lines().collect();
2406        let ranges = production_fn_ranges(&lines, &[]);
2407        assert_eq!(ranges.len(), 1);
2408        let (name, start, end) = &ranges[0];
2409        assert_eq!(name, "read_adapter_binding");
2410        assert_eq!(*start, 0);
2411        assert_eq!(*end, lines.len() - 1);
2412        assert_eq!(
2413            enclosing_fn(3, &ranges),
2414            Some("read_adapter_binding".to_string())
2415        );
2416    }
2417
2418    /// Build the `facts`/`parents` vectors [`file_is_named_fs_floor`] now takes as
2419    /// caller-supplied arguments, the same way [`fs_below_driver`] does, so each test
2420    /// below reads as "classify this file" rather than repeating the setup.
2421    fn classify(krate: &str, files: &[(PathBuf, String)], i: usize) -> bool {
2422        let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
2423        let parents: Vec<Option<usize>> =
2424            files.iter().map(|(p, _)| module_parent(p, files)).collect();
2425        file_is_named_fs_floor(krate, files, &facts, &parents, i)
2426    }
2427
2428    /// The concrete #1104 shape, in miniature: `project.rs`'s bare `use std::fs;` (no
2429    /// enclosing fn — never itself a violation) plus `discovery.rs`'s two named-exception
2430    /// functions. The whole file must read as a named floor, not residual.
2431    #[test]
2432    fn file_is_named_fs_floor_true_for_the_real_discovery_rs_shape() {
2433        let files = [
2434            (
2435                PathBuf::from("project.rs"),
2436                "use std::fs;\n\nmod discovery;\n".to_string(),
2437            ),
2438            (
2439                PathBuf::from("project/discovery.rs"),
2440                "use super::*;\n\npub(crate) fn discover_bynk_files() {\n    let _ = fs::read_dir(\".\");\n}\n\npub(crate) fn read_adapter_binding(path: &Path) -> std::io::Result<String> {\n    fs::read_to_string(path)\n}\n".to_string(),
2441            ),
2442        ];
2443        assert!(classify("bynk-emit", &files, 1));
2444    }
2445
2446    /// A new, unlisted fn touching `std::fs` in the *same file* as two named exceptions
2447    /// must flip the whole file to residual — no partial credit, since "named floor"
2448    /// must mean every touch is accounted for, not most of them.
2449    #[test]
2450    fn file_is_named_fs_floor_false_when_an_unnamed_fn_also_touches_fs() {
2451        let files = [
2452            (
2453                PathBuf::from("project.rs"),
2454                "use std::fs;\n\nmod discovery;\n".to_string(),
2455            ),
2456            (
2457                PathBuf::from("project/discovery.rs"),
2458                "use super::*;\n\npub(crate) fn discover_bynk_files() {\n    let _ = fs::read_dir(\".\");\n}\n\nfn some_new_helper() {\n    let _ = fs::write(\"x\", \"y\");\n}\n".to_string(),
2459            ),
2460        ];
2461        assert!(!classify("bynk-emit", &files, 1));
2462    }
2463
2464    /// A file whose only production-scope touch is a bare `use std::fs;` import — no
2465    /// enclosing fn at all — is trivially a named floor: the import performs no I/O by
2466    /// itself, and the descendant it enables is checked (and named) separately.
2467    #[test]
2468    fn file_is_named_fs_floor_true_for_an_import_only_file() {
2469        let files = [(
2470            PathBuf::from("project.rs"),
2471            "use std::fs;\n\nmod discovery;\n".to_string(),
2472        )];
2473        assert!(classify("bynk-emit", &files, 0));
2474    }
2475
2476    /// The same `discovery.rs` shape under the wrong crate label must not read as a
2477    /// floor — [`NAMED_FS_EXCEPTIONS`] is keyed on `(crate, file, fn)`, not `(file, fn)`
2478    /// alone, so a same-named file/fn pair in a different crate isn't accidentally
2479    /// covered.
2480    #[test]
2481    fn file_is_named_fs_floor_false_under_the_wrong_crate() {
2482        let files = [
2483            (
2484                PathBuf::from("project.rs"),
2485                "use std::fs;\n\nmod discovery;\n".to_string(),
2486            ),
2487            (
2488                PathBuf::from("project/discovery.rs"),
2489                "use super::*;\n\npub(crate) fn discover_bynk_files() {\n    let _ = fs::read_dir(\".\");\n}\n".to_string(),
2490            ),
2491        ];
2492        assert!(!classify("bynk-ide", &files, 1));
2493    }
2494
2495    /// Review finding (#1106): a module-scope `std::fs` touch that isn't an import
2496    /// declaration — a `static` initialiser doing real I/O — has no enclosing fn either,
2497    /// but is a genuine R2.3 violation and must not be waved through as a floor just
2498    /// because it sits outside every known fn range.
2499    #[test]
2500    fn file_is_named_fs_floor_false_for_a_module_scope_static_that_reads() {
2501        let files = [(
2502            PathBuf::from("project.rs"),
2503            "use std::fs;\n\nstatic ROOT: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| fs::read_to_string(\"x\").unwrap());\n"
2504                .to_string(),
2505        )];
2506        assert!(!classify("bynk-emit", &files, 0));
2507    }
2508
2509    /// Same review finding, the [`fn_name_on_line`] half: an `extern "C" fn` (a modifier
2510    /// combination the parser doesn't strip) produces no [`production_fn_ranges`] entry
2511    /// at all, so its whole body would fall into the "no enclosing fn" branch. It must
2512    /// still read as residual, not floor, once it touches `std::fs`.
2513    #[test]
2514    fn file_is_named_fs_floor_false_for_an_unparsed_extern_fn_body() {
2515        let files = [(
2516            PathBuf::from("project.rs"),
2517            "use std::fs;\n\nextern \"C\" fn callback() {\n    let _ = fs::read_dir(\".\");\n}\n"
2518                .to_string(),
2519        )];
2520        assert!(!classify("bynk-emit", &files, 0));
2521    }
2522
2523    // --- emit_abi_shapes (#999 Decision E) ----------------------------------
2524
2525    /// A binding's ordinary capability-interface imports, and the emit-ABI tag-layout
2526    /// names, must not be flagged — the exact failure mode Decision E rebuilt the probe
2527    /// to avoid (the original single-allowlist definition read 29-33 here, not 1).
2528    ///
2529    /// Exercises the real production allowlists via [`is_enumerated_emit_abi_or_capability_surface`]
2530    /// — not a local re-declaration. A test with its own copy of `EMIT_ABI` would still
2531    /// pass if the real one lost an entry (e.g. deleting `Uuid` from the production
2532    /// list), proving nothing about the probe it claims to cover.
2533    #[test]
2534    fn emit_abi_shapes_does_not_flag_capability_or_tag_layout_imports() {
2535        let src = "import type { Clock, Fetch, Locale } from \"./bynk.js\";\n\
2536                    import { FetchError, Uuid } from \"./bynk.js\";\n\
2537                    import { Err, None, Ok, Some, type Option, type Result } from \"./runtime.js\";\n";
2538        let imports = ts_named_imports_from_runtime_modules(src);
2539        let leaks: Vec<&String> = imports
2540            .iter()
2541            .filter(|i| !is_enumerated_emit_abi_or_capability_surface(i))
2542            .collect();
2543        assert!(leaks.is_empty(), "unexpected leaks: {leaks:?}");
2544    }
2545
2546    /// The falsifier from #999 Decision E, checked directly: deleting an entry from the
2547    /// real production allowlist must be detectable by *some* test — this one flags
2548    /// `Uuid` as a leak the moment it's removed from [`EMIT_ABI`], which the test above
2549    /// (using the real const) would also start failing on.
2550    #[test]
2551    fn is_enumerated_checks_the_real_production_allowlist() {
2552        assert!(is_enumerated_emit_abi_or_capability_surface("Uuid"));
2553        assert!(is_enumerated_emit_abi_or_capability_surface("LocaleTag"));
2554        assert!(!is_enumerated_emit_abi_or_capability_surface(
2555            "negotiateLocale"
2556        ));
2557    }
2558
2559    /// The real, current-tree finding this probe exists to surface: `negotiateLocale`,
2560    /// a plain value helper from `./runtime.js` alongside the tag-layout constructors,
2561    /// is neither an enumerated emit-ABI shape nor a capability-interface import.
2562    #[test]
2563    fn emit_abi_shapes_flags_a_non_enumerated_runtime_helper() {
2564        let src = "import { Err, None, Ok, Some, negotiateLocale, type Option, type Result } from \"./runtime.js\";\n";
2565        let imports = ts_named_imports_from_runtime_modules(src);
2566        assert!(imports.contains(&"negotiateLocale".to_string()));
2567    }
2568
2569    /// `FetchError` is `import type` in one binding and a plain value import in
2570    /// another (`FetchError.Timeout`) — Decision E's rejected type-vs-value
2571    /// discriminator. Confirms the extractor treats both forms as the same identifier,
2572    /// so the allowlist check doesn't depend on which form a given file happens to use.
2573    #[test]
2574    fn ts_import_extraction_ignores_type_only_vs_value_distinction() {
2575        let type_only = "import type { FetchError } from \"./bynk.js\";\n";
2576        let value = "import { FetchError, Uuid } from \"./bynk.js\";\n";
2577        assert_eq!(
2578            ts_named_imports_from_runtime_modules(type_only),
2579            vec!["FetchError".to_string()]
2580        );
2581        assert!(ts_named_imports_from_runtime_modules(value).contains(&"FetchError".to_string()));
2582    }
2583
2584    // --- options_sources -----------------------------------------------------
2585
2586    #[test]
2587    fn struct_body_finds_a_field_by_name() {
2588        let src = "struct Foo {\n    pub sources: Option<HashMap<PathBuf, String>>,\n    pub other: bool,\n}\n";
2589        let body = struct_body(src, "Foo").expect("struct body found");
2590        assert!(body.contains("sources"));
2591    }
2592
2593    #[test]
2594    fn struct_body_does_not_match_an_unrelated_struct() {
2595        let src =
2596            "struct Bar {\n    pub sources: bool,\n}\n\nstruct Foo {\n    pub other: bool,\n}\n";
2597        let body = struct_body(src, "Foo").expect("struct body found");
2598        assert!(!body.contains("sources"));
2599    }
2600
2601    // --- render_table's "Rules closed" section (#1001) ------------------------
2602
2603    fn empty_report() -> Report {
2604        Report { probes: Vec::new() }
2605    }
2606
2607    /// The section is static text — no count, no existence check — precisely
2608    /// because nothing regenerates `design/greenfield-status.md` when `stamp`
2609    /// writes the ledger, so a computed count would silently go stale the
2610    /// moment the first `closes_rule` landed (the drift a first draft of this
2611    /// section introduced, caught in #1001's review). This test pins "static"
2612    /// as the actual behaviour, not just the intent in a comment.
2613    #[test]
2614    fn render_table_rules_closed_section_is_static_regardless_of_the_tree() {
2615        let out = render_table(&empty_report());
2616        assert!(out.contains("greenfield-status-rules.md"), "{out}");
2617        assert!(
2618            out.contains("may not exist yet"),
2619            "the wording must not claim to know whether the ledger exists: {out}"
2620        );
2621    }
2622}