Skip to main content

xtask/
greenfield_status.rs

1//! `cargo xtask greenfield-status` — the probe harness (track doc §8, proposal #999).
2//!
3//! Fifteen probes measuring the tree against `design/bynk-greenfield-compiler.md`:
4//! the twelve in track doc §8, `emit_abi_shapes` (ADR 0310's probe, #999 Decision E —
5//! this slice measures the emit-ABI enumeration guard but does not wire it; wiring is
6//! packaging-track work), and `ts_writes`/`ts_any` (phase 7's own probes, P7.0/#1296 —
7//! `design/tracks/the-typescript-tree.md` §5).
8//!
9//! **Eleven are gated**, committed and diffed: `workspace_lints`, `fs_below_driver`,
10//! `options_sources`, `hoist_sinks`, `span_keyed_maps`, `emit_diagnostics`,
11//! `ide_emit_edge`, `ast_importers`, `emit_abi_shapes`, `ts_writes`, `ts_any`. Nine of
12//! these are zero/closure-shaped — a boolean, or a count pinned at a small, argued
13//! floor (`ast_importers` = 5, `emit_abi_shapes` = 1). `ts_writes`/`ts_any` are not:
14//! they read 1641/55, converging toward a floor named at `the-typescript-tree.md`'s own
15//! retirement over dozens of slices, the same shape `ast_importers` had throughout
16//! phase 6's 59 — gated despite the churn that implies, a deliberate call argued in
17//! `design/pending/p7-0-ts-writes-ts-any-probes.md`'s own ADR (review of #1297), not an
18//! oversight of #999 Decision D's churn-avoidance principle. A disagreement between a
19//! fresh run and the committed table fails `greenfield_status_table_is_current`
20//! (`xtask/tests/greenfield_status.rs`), which rides both the `test` job (`cargo test
21//! --workspace`, any Rust-touching PR) and the `drift` job's existing `cargo test -p
22//! xtask` (pending/decisions-only PRs) — no new CI wiring (#999 Decision D, which also
23//! explains why a `drift`-job *step* would have been silently skipped on the PRs that
24//! move these probes most).
25//!
26//! **Four are count/ratio trend probes**, recomputed and printed but never diffed:
27//! `wildcard_arms`, `keep_in_sync`, `test_density`, `fixture_kinds`. These move on
28//! nearly any ordinary Rust PR with no slice actively driving them toward a floor (§8
29//! calls two of them "trends, not gates"); hard-gating them would make the committed
30//! table churn, and conflict, on routine work for no corresponding benefit.
31//!
32//! `Closes-Rule:` rule-id provenance (#999 Decision B) is deferred to a follow-on
33//! slice — the committed table below carries no rule-citation column yet.
34
35use std::collections::BTreeSet;
36use std::fmt::Write as _;
37use std::path::{Path, PathBuf};
38use std::process::Command;
39
40/// One probe's result. `gated` probes are diffed against the committed table by
41/// [`crate::greenfield_status::gated_disagreements`]; the rest are reported only.
42pub struct Probe {
43    pub name: &'static str,
44    pub gated: bool,
45    pub reads: String,
46}
47
48pub struct Report {
49    pub probes: Vec<Probe>,
50}
51
52impl Report {
53    pub fn get(&self, name: &str) -> &str {
54        self.probes
55            .iter()
56            .find(|p| p.name == name)
57            .map(|p| p.reads.as_str())
58            .unwrap_or_else(|| panic!("no probe named {name:?}"))
59    }
60}
61
62/// Run every probe against the tree rooted at `root` (the repo root). Used by the CLI's
63/// full report; the gating test uses the thirteen gated probes alone
64/// ([`gated_disagreements`]) so it never pays for a workspace-wide clippy pass
65/// (`wildcard_arms`) just to check the probes that are actually diffed.
66pub fn run(root: &Path) -> Report {
67    let mut probes = run_gated(root);
68    probes.extend(run_trend(root));
69    Report { probes }
70}
71
72/// The thirteen gated (zero/closure) probes only — what [`gated_disagreements`] diffs.
73fn run_gated(root: &Path) -> Vec<Probe> {
74    vec![
75        workspace_lints(root),
76        fs_below_driver(root),
77        options_sources(root),
78        hoist_sinks(root),
79        span_keyed_maps(root),
80        emit_diagnostics(root),
81        ide_emit_edge(root),
82        ast_importers(root),
83        emit_abi_shapes(root),
84        ts_writes(root),
85        ts_any(root),
86        verbatim_origins(root),
87        verbatim_sites(root),
88    ]
89}
90
91/// The four reported-only trend probes — never diffed, and notably including the one
92/// (`wildcard_arms`) that shells out to a full `cargo clippy --workspace` pass, which
93/// the gating test must not pay for on every run.
94fn run_trend(root: &Path) -> Vec<Probe> {
95    vec![
96        wildcard_arms(root),
97        keep_in_sync(root),
98        test_density(root),
99        fixture_kinds(root),
100    ]
101}
102
103/// `design/greenfield-status.md` — the committed table this probe set regenerates.
104pub fn table_path(root: &Path) -> PathBuf {
105    root.join("design/greenfield-status.md")
106}
107
108// --- Filesystem helpers --------------------------------------------------
109
110/// Every `.rs` file under `dir`, recursively, as `(path, contents)`. Unreadable files
111/// (permissions, non-UTF-8) are skipped rather than failing the whole walk — this is a
112/// measurement tool, not a build step.
113fn rust_files(dir: &Path) -> Vec<(PathBuf, String)> {
114    let mut out = Vec::new();
115    walk(dir, &mut out);
116    out
117}
118
119fn walk(dir: &Path, out: &mut Vec<(PathBuf, String)>) {
120    let Ok(entries) = std::fs::read_dir(dir) else {
121        return;
122    };
123    let mut entries: Vec<_> = entries.flatten().collect();
124    entries.sort_by_key(|e| e.file_name());
125    for entry in entries {
126        let path = entry.path();
127        if path.is_dir() {
128            walk(&path, out);
129        } else if path.extension().is_some_and(|e| e == "rs")
130            && let Ok(contents) = std::fs::read_to_string(&path)
131        {
132            out.push((path, contents));
133        }
134    }
135}
136
137/// The inner text of every **standalone** `"bynk.<ident>"` string literal (the
138/// `bynk.*` convention used for diagnostic codes and commons/namespace paths alike).
139///
140/// Standalone, not merely prefix-matching: the identifier run must be immediately
141/// followed by the closing quote, matching the naive `rg -o '"bynk\.[a-zA-Z0-9_.]*"'`
142/// this probe is deliberately more careful than (#999 Decision A). Without that
143/// requirement this would also match the *start* of an unrelated, longer message that
144/// merely happens to begin with "bynk." — e.g. a panic string
145/// `"bynk.map itself uses bynk.list, so list must be injected too: {paths:?}"` is prose
146/// beginning with a namespace-shaped word, not a `"bynk.map"` code literal, and a
147/// dev-only compile-time error message split across lines with a `\`-continuation
148/// (`"bynk.emit.unresolved_cross_context_signature: no signature for \` ...) is one
149/// string, not a diagnostic-code literal, even though its first segment matches the
150/// identifier charset. Both were found — and wrongly counted — by an earlier,
151/// less careful version of this scan; the fix is requiring the closing quote.
152fn bynk_dotted_literals(src: &str) -> Vec<&str> {
153    let mut out = Vec::new();
154    let bytes = src.as_bytes();
155    let mut i = 0;
156    while let Some(rel) = src[i..].find("\"bynk.") {
157        let start = i + rel + 1; // skip the opening quote
158        let mut end = start;
159        while end < bytes.len()
160            && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'.')
161        {
162            end += 1;
163        }
164        if end < bytes.len() && bytes[end] == b'"' {
165            out.push(&src[start..end]);
166        }
167        i = end.max(start + 1);
168    }
169    out
170}
171
172/// True if `line`, trimmed, is a `//` or `///` or `//!` line comment. Doesn't attempt
173/// block comments (`/* */`) — none of this codebase's `bynk.*`/dead-identifier
174/// mentions live in one.
175fn is_line_comment(line: &str) -> bool {
176    line.trim_start().starts_with("//")
177}
178
179// --- Gated probe 1: workspace_lints --------------------------------------
180
181/// R2.12. `[workspace.lints]` presence and `clippy::wildcard_enum_match_arm`'s level in
182/// the root `Cargo.toml`. A boolean-shaped probe (not a count) — gated because it only
183/// ever changes once, when T0.3 lands it.
184fn workspace_lints(root: &Path) -> Probe {
185    let cargo_toml = std::fs::read_to_string(root.join("Cargo.toml")).unwrap_or_default();
186    let has_section = cargo_toml
187        .lines()
188        .any(|l| l.trim() == "[workspace.lints.clippy]" || l.trim() == "[workspace.lints]");
189    let level = cargo_toml
190        .lines()
191        .find(|l| l.contains("wildcard_enum_match_arm"))
192        .map(|l| l.trim().to_string());
193    let reads = match (has_section, level) {
194        (true, Some(l)) => format!("present — {l}"),
195        (true, None) => "present, wildcard_enum_match_arm not set".to_string(),
196        (false, _) => "absent".to_string(),
197    };
198    Probe {
199        name: "workspace_lints",
200        gated: true,
201        reads,
202    }
203}
204
205// --- Gated probe 2: fs_below_driver --------------------------------------
206
207/// R2.3. Files under `bynk-emit/src`, `bynk-ide/src`, `bynk-fmt/src` (the crates below
208/// the `bynk` driver, which owns disk I/O) that touch `std::fs` in **production** code.
209///
210/// Excludes usage inside a trailing `#[cfg(test)] mod tests { ... }` block — the
211/// convention every file in this codebase uses, always the last item in the file. A
212/// line is production-scope unless it falls at or after the line following a
213/// `#[cfg(test)]` attribute whose very next non-empty line opens a `mod ... {` block
214/// (as opposed to a `mod name;` external-file declaration, which is not a scope at
215/// all). This mirrors the comment-exclusion discipline elsewhere in this probe set:
216/// tests writing fixtures to a tempdir are not "the driver's job" bypassed, and
217/// counting them would report a rule open that the production code has already closed.
218///
219/// A file counts if its own text names `std::fs` ([`has_production_std_fs`]), **or** if
220/// a bare `fs::`-style call site in it resolves to `std::fs` through its imports
221/// ([`production_std_fs_files`]) — a module-level `use std::fs;` in a parent module is
222/// visible to a child through `use super::*;` (module privacy is ancestor-scoped), so
223/// `bynk-emit/src/project/discovery.rs` reads and walks the filesystem while never
224/// spelling `std::fs` itself. The literal text scan alone missed exactly that file,
225/// so a probe reading `bynk-emit=0` would have asserted R2.3 closed on a false
226/// premise (#1013).
227///
228/// #1104 (a content-ownership (#1086) probe-precision follow-on): a flagged *count*
229/// alone can't tell a residual R2.3 violation from a documented, permanent exception —
230/// `bynk-emit`'s 3 have read that way since the track's retirement (`design/archive/
231/// retired-tracks.md`'s closing summary), each named in [`NAMED_FS_EXCEPTIONS`]. So
232/// each flagged file is additionally classified as a **named floor** file — every
233/// production-scope touch it has is either inside one of those named functions, or is
234/// a bare import declaration (no fn encloses it — [`enclosing_fn`] returns `None`) that
235/// performs no I/O of its own, existing only so a *descendant* module's bare `fs::`
236/// call can resolve (exactly `project.rs`'s `use std::fs;`, which `discovery.rs` and
237/// `paths.rs` glob-import via `use super::*;`) — or a **residual** file: any other file
238/// touching `std::fs` in production scope, which still reads as a real R2.3 violation
239/// ([`file_is_named_fs_floor`]).
240fn fs_below_driver(root: &Path) -> Probe {
241    let crates = ["bynk-emit", "bynk-ide", "bynk-fmt"];
242    let mut per_crate = Vec::new();
243    let mut total = 0usize;
244    let mut total_floor = 0usize;
245    for krate in crates {
246        let dir = root.join(krate).join("src");
247        let files: Vec<(PathBuf, String)> = rust_files(&dir)
248            .into_iter()
249            .map(|(path, contents)| {
250                let rel = path.strip_prefix(&dir).unwrap_or(&path).to_path_buf();
251                (rel, contents)
252            })
253            .collect();
254        let flagged = production_std_fs_files(&files);
255        let count = flagged.len();
256        total += count;
257        let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
258        let parents: Vec<Option<usize>> = files
259            .iter()
260            .map(|(p, _)| module_parent(p, &files))
261            .collect();
262        let floor = flagged
263            .iter()
264            .filter(|&&i| file_is_named_fs_floor(krate, &files, &facts, &parents, i))
265            .count();
266        total_floor += floor;
267        let residual = count - floor;
268        per_crate.push(if floor > 0 {
269            format!("{krate}={count} ({floor} named floor, {residual} residual)")
270        } else {
271            format!("{krate}={count}")
272        });
273    }
274    Probe {
275        name: "fs_below_driver",
276        gated: true,
277        reads: format!(
278            "{total} files ({}) — {total_floor} named floor, {} residual total",
279            per_crate.join(", "),
280            total - total_floor
281        ),
282    }
283}
284
285/// #1104: the specific, permanently-carved-out production functions whose
286/// `std::fs` touch is a *named* exception, not evidence of unfinished R2.3
287/// migration — settled in `design/tracks/content-ownership.md` §3.2 (retired) and
288/// its closing summary in `design/archive/retired-tracks.md`. `(crate, file path
289/// relative to that crate's `src/`, enclosing production fn name)`. A future
290/// carve-out decided the same deliberate way joins this list; anything touching
291/// `std::fs` in production scope that isn't listed here reads as a residual R2.3
292/// violation, per [`file_is_named_fs_floor`].
293const NAMED_FS_EXCEPTIONS: &[(&str, &str, &str)] = &[
294    // The bare enumeration walk — no content read, no overlay parameter at all.
295    ("bynk-emit", "project/discovery.rs", "discover_bynk_files"),
296    // An adapter's `.binding.ts` path is only known post-parse, so no discovery walk
297    // can pre-populate it into a caller-supplied overlay the way `.bynk` files are.
298    ("bynk-emit", "project/discovery.rs", "read_adapter_binding"),
299    // The plain, no-overlay manifest reader's contract has always been "read the real
300    // file"; nothing above it in the call chain can supply this for a caller that
301    // doesn't build its own overlay.
302    ("bynk-emit", "project/paths.rs", "try_read_project_paths"),
303];
304
305/// Is flagged file `files[i]` (already known, by [`production_std_fs_files`], to touch
306/// `std::fs` in production scope) a **named floor** file — every production-scope touch
307/// it has is either inside a [`NAMED_FS_EXCEPTIONS`] function for this exact
308/// `(krate, file)`, or a bare `use` import declaration (which reads but performs no
309/// filesystem operation by itself, unlike a module-scope `static`/`const` initialiser or
310/// macro invocation that might)? `facts`/`parents` are the caller's already-computed
311/// [`fs_import_facts`]/[`module_parent`] vectors for `files`, threaded through rather
312/// than recomputed per flagged file.
313///
314/// A single disallowed touch — inside an unlisted fn, inside a listed fn's *file* but
315/// wrong *name*, or outside every fn and not a plain import — makes the whole file
316/// residual: partial credit isn't meaningful here, since the point is "can a reader stop
317/// cross-referencing track docs for this file," not a ratio. Likewise, a file this
318/// function attributes *no* touch line to at all (despite the caller already knowing it's
319/// flagged — [`line_touches_std_fs`]'s re-implementation of the file-level detection
320/// disagreeing with it) reads as residual, not floor: an unattributable touch means this
321/// classifier doesn't understand the file, which must fail loud, not quiet.
322fn file_is_named_fs_floor(
323    krate: &str,
324    files: &[(PathBuf, String)],
325    facts: &[FsImportFacts],
326    parents: &[Option<usize>],
327    i: usize,
328) -> bool {
329    let (path, _) = &files[i];
330    let rel = path.to_string_lossy().replace('\\', "/");
331    let lines: Vec<&str> = files[i].1.lines().collect();
332    let ranges = test_mod_ranges(&lines);
333    let fn_ranges = production_fn_ranges(&lines, &ranges);
334
335    let mut saw_touch = false;
336    for (li, line) in lines.iter().enumerate() {
337        if in_test_range(li, &ranges) {
338            continue;
339        }
340        if !line_touches_std_fs(i, line, facts, parents, files) {
341            continue;
342        }
343        saw_touch = true;
344        let Some(fn_name) = enclosing_fn(li, &fn_ranges) else {
345            // No enclosing fn is harmless only when the line is literally an import
346            // declaration. A module-scope `static`/`const` initialiser, a macro
347            // invocation, or a fn shape `fn_name_on_line` can't parse (`extern "C" fn`)
348            // does real I/O outside every known range and must read as residual.
349            if use_declaration(line).is_some() {
350                continue;
351            }
352            return false;
353        };
354        let named = NAMED_FS_EXCEPTIONS
355            .iter()
356            .any(|&(c, f, func)| c == krate && f == rel && func == fn_name);
357        if !named {
358            return false;
359        }
360    }
361    saw_touch
362}
363
364/// Does `line` (already known to be production-scope) itself touch `std::fs` — by the
365/// same two means [`production_std_fs_files`] checks at file granularity, applied here
366/// to one line: a literal `std::fs` substring, or a bare/qualified path this line spells
367/// that resolves to `std::fs` through file `i`'s visible import bindings.
368fn line_touches_std_fs(
369    i: usize,
370    line: &str,
371    facts: &[FsImportFacts],
372    parents: &[Option<usize>],
373    files: &[(PathBuf, String)],
374) -> bool {
375    if line.contains("std::fs") {
376        return true;
377    }
378    let mut roots = BTreeSet::new();
379    collect_bare_path_roots(line, &mut roots);
380    if roots.iter().any(|name| {
381        matches!(
382            resolve_name_in_module(i, name, facts, parents),
383            NameResolution::StdFs
384        )
385    }) {
386        return true;
387    }
388    let mut chains = BTreeSet::new();
389    collect_qualified_paths(line, &mut chains);
390    chains
391        .iter()
392        .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
393}
394
395/// The name and inclusive body line-range of every production-scope `fn` in `lines`
396/// (`test_ranges` excluded, same as everywhere else in this probe) — used by
397/// [`file_is_named_fs_floor`] to attribute a flagged touch line to its enclosing
398/// function. A wrapped signature (the `{` arriving lines after the `fn` line, past a
399/// multi-line parameter list) is handled the same way [`test_mod_ranges`] handles a
400/// `mod` line: brace depth is tracked starting at the `fn` line itself, but a parameter
401/// list has no `{`/`}` in it, so `started` only flips true once the real body-opening
402/// brace arrives, however many lines later.
403fn production_fn_ranges(
404    lines: &[&str],
405    test_ranges: &[(usize, usize)],
406) -> Vec<(String, usize, usize)> {
407    let mut out = Vec::new();
408    for (i, line) in lines.iter().enumerate() {
409        if in_test_range(i, test_ranges) {
410            continue;
411        }
412        let Some(name) = fn_name_on_line(line) else {
413            continue;
414        };
415        let mut state = BraceScanState::Normal;
416        let mut depth = 0i32;
417        let mut started = false;
418        let mut end = lines.len() - 1;
419        for (j, l) in lines[i..].iter().enumerate() {
420            let (delta, new_state) = brace_delta(l, state);
421            state = new_state;
422            depth += delta;
423            if depth != 0 {
424                started = true;
425            }
426            if started && depth == 0 {
427                end = i + j;
428                break;
429            }
430        }
431        out.push((name, i, end));
432    }
433    out
434}
435
436/// The leading `fn NAME` on `line`, past an optional `pub`/`pub(...)`, `async`,
437/// `unsafe`, `const` modifier run (in any order/repetition, mirroring
438/// [`collect_declared_type_name`]'s `pub`-stripping) — `None` if `line` doesn't open a
439/// function at all (a call site, a doc comment mentioning "fn", a closure). Doesn't
440/// require a trailing `{` or even `(` on this same line — a wrapped signature's `fn`
441/// line can end right at the name.
442fn fn_name_on_line(line: &str) -> Option<String> {
443    let mut t = line.trim();
444    loop {
445        if let Some(rest) = t.strip_prefix("pub") {
446            let rest = rest.trim_start();
447            t = if let Some(after_paren) = rest.strip_prefix('(') {
448                after_paren.split_once(')')?.1.trim_start()
449            } else {
450                rest
451            };
452            continue;
453        }
454        let mut advanced = false;
455        for kw in ["async ", "unsafe ", "const "] {
456            if let Some(rest) = t.strip_prefix(kw) {
457                t = rest.trim_start();
458                advanced = true;
459                break;
460            }
461        }
462        if !advanced {
463            break;
464        }
465    }
466    let rest = t.strip_prefix("fn ")?;
467    let end = rest
468        .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
469        .unwrap_or(rest.len());
470    if end == 0 {
471        return None;
472    }
473    Some(rest[..end].to_string())
474}
475
476/// The innermost [`production_fn_ranges`] entry containing `line_idx`, by name — `None`
477/// if `line_idx` sits outside every production fn (module scope: a `use` declaration,
478/// a `const`/`static`, or a `struct`/`enum` body).
479fn enclosing_fn(line_idx: usize, fn_ranges: &[(String, usize, usize)]) -> Option<String> {
480    fn_ranges
481        .iter()
482        .filter(|(_, start, end)| line_idx >= *start && line_idx <= *end)
483        .min_by_key(|(_, start, end)| end - start)
484        .map(|(name, _, _)| name.clone())
485}
486
487/// The literal text component of [`fs_below_driver`]: some production-scope line names
488/// `std::fs`. Necessary but not sufficient (#1013) — a file can touch `std::fs`
489/// through a glob-imported parent binding without ever spelling it; that resolution
490/// lives in [`production_std_fs_files`], which layers on top of this scan.
491fn has_production_std_fs(src: &str) -> bool {
492    let lines: Vec<&str> = src.lines().collect();
493    let ranges = test_mod_ranges(&lines);
494    for (i, line) in lines.iter().enumerate() {
495        if in_test_range(i, &ranges) {
496            continue;
497        }
498        if line.contains("std::fs") {
499            return true;
500        }
501    }
502    false
503}
504
505/// Indices (into `files`, whose paths are relative to the crate's `src/` root) of the
506/// files that touch `std::fs` in production code — the union of the literal text scan
507/// ([`has_production_std_fs`]) and import resolution: a path whose leading module
508/// segment a production `use` declaration binds to `std::fs` (or an item under it),
509/// either a bare `NAME::` root resolved in the file itself (`use std::{fs, io};` — a
510/// form the substring scan can't see) or in an ancestor module reached through
511/// `use super::*;`, transitively (#1013), or a `super::`/`self::`/`crate::`-qualified
512/// path walked through the module tree to the same bindings (#1016 review — a
513/// descendant may spell `super::fs::read_to_string(p)` with no glob import at all,
514/// one disambiguating edit away from a currently-flagged bare call).
515///
516/// Resolution is Rust-shaped, not hand-tracked (#1013 rejects a special-case list):
517/// a private `use std::fs;` in a parent is visible to descendants because module
518/// privacy is ancestor-scoped, a chain of `use super::*;` globs re-reaches it from
519/// any depth, and a nearer binding of the same name shadows a farther one — whether
520/// that binding is another `use` or a locally-declared type-namespace item (`mod fs;`,
521/// `struct File`, …; value-namespace items like `fn` can't head a `NAME::` path, so
522/// they don't shadow one) — so a child that binds `fs` to something else keeps its
523/// bare `fs::` calls unflagged. Visibility is *not* modelled: a path that names a
524/// too-private binding wouldn't compile anyway, so over-approximating is safe.
525///
526/// Known remaining gaps, accepted as out of reach for a text-level scanner: an
527/// ancestor's `use std::fs::read_to_string;` item import called bare (`read_to_string(p)`)
528/// presents no `::` path segment to resolve — the same import used as a path root
529/// (`File::open`) **is** caught, since item bindings under `std::fs` participate in
530/// the same resolution — and a `use` declaration rustfmt has split across lines is
531/// not parsed. #1013 grepped the three scanned crates for the item-import form, and
532/// the #1016 review for the qualified-path and split-declaration forms — zero hits.
533fn production_std_fs_files(files: &[(PathBuf, String)]) -> Vec<usize> {
534    let facts: Vec<FsImportFacts> = files.iter().map(|(_, src)| fs_import_facts(src)).collect();
535    let parents: Vec<Option<usize>> = files
536        .iter()
537        .map(|(path, _)| module_parent(path, files))
538        .collect();
539    (0..files.len())
540        .filter(|&i| {
541            has_production_std_fs(&files[i].1)
542                || resolves_bare_std_fs(i, &facts, &parents)
543                || resolves_qualified_std_fs(i, &facts, &parents, files)
544        })
545        .collect()
546}
547
548/// Per-file production-scope import facts for [`production_std_fs_files`]'s
549/// resolution. All fields exclude `#[cfg(test)] mod` ranges — a test module's
550/// `use super::*;` or tempdir `fs::write` must not make the file, or its children,
551/// read as production `std::fs` (the `bynk-ide` files' shape).
552#[derive(Default)]
553struct FsImportFacts {
554    /// A production `use super::*;` (optionally `pub`-qualified) — the edge that lets
555    /// this file see its parent module's `use` bindings, and (chained) its ancestors'.
556    glob_imports_super: bool,
557    /// Names production `use` declarations bind to `std::fs` or an item under it:
558    /// `use std::fs;` → `fs`, `use std::fs as x;` → `x`, `use std::{fs, io};` → `fs`,
559    /// `use std::fs::File;` → `File`.
560    std_fs_bindings: BTreeSet<String>,
561    /// Every name a production `use` declaration binds, whatever the target — the
562    /// shadow set: a nearer non-`std::fs` binding of a candidate name stops resolution.
563    use_bound_names: BTreeSet<String>,
564    /// Type-namespace items the file declares (`mod fs;`, `struct File`, `enum`,
565    /// `trait`, `type`, `union`) — these beat a glob-imported name in real Rust, so
566    /// they join [`Self::use_bound_names`] on the shadow side of resolution (#1016
567    /// review). Value-namespace items (`fn`, `const`, `static`) can't head a `NAME::`
568    /// module path and are deliberately not collected.
569    declared_type_names: BTreeSet<String>,
570    /// Identifiers appearing as a bare path root `NAME::` (not preceded by another
571    /// path segment) on a production line — the call-site side of the resolution.
572    bare_path_roots: BTreeSet<String>,
573    /// Segment chains of `super::`/`self::`/`crate::`-qualified paths on production
574    /// lines — `super::fs::read_to_string` records `["super", "fs", "read_to_string"]`.
575    /// These need no glob import to reach an ancestor's binding (#1016 review).
576    qualified_paths: BTreeSet<Vec<String>>,
577}
578
579fn fs_import_facts(src: &str) -> FsImportFacts {
580    let lines: Vec<&str> = src.lines().collect();
581    let ranges = test_mod_ranges(&lines);
582    let mut facts = FsImportFacts::default();
583    for (i, line) in lines.iter().enumerate() {
584        if in_test_range(i, &ranges) {
585            continue;
586        }
587        if let Some(decl) = use_declaration(line) {
588            if decl == "super::*" {
589                facts.glob_imports_super = true;
590            }
591            collect_use_bindings("", decl, &mut facts);
592        }
593        collect_declared_type_name(line, &mut facts.declared_type_names);
594        collect_bare_path_roots(line, &mut facts.bare_path_roots);
595        collect_qualified_paths(line, &mut facts.qualified_paths);
596    }
597    facts
598}
599
600/// The path text of a single-line `use` declaration — `use std::fs;` → `std::fs`,
601/// with an optional `pub`/`pub(crate)`/`pub(in …)` prefix stripped and a trailing
602/// `//` comment tolerated (`use super::*; // parent's fs` must not silently sever
603/// the glob edge for a whole subtree — #1016 review; safe to split on `//` because
604/// a `use` path can contain neither a comment marker nor a string). A declaration
605/// rustfmt has split across lines has no trailing `;` here and is not recognised —
606/// none of the `std::fs` forms in the scanned crates are long enough to split.
607fn use_declaration(line: &str) -> Option<&str> {
608    let mut t = line.trim();
609    if let Some(rest) = t.strip_prefix("pub") {
610        let rest = rest.trim_start();
611        t = if let Some(after_paren) = rest.strip_prefix('(') {
612            after_paren.split_once(')')?.1.trim_start()
613        } else {
614            rest
615        };
616    }
617    let body = t.strip_prefix("use ")?;
618    let body = body.split("//").next().unwrap_or(body);
619    body.trim().strip_suffix(';').map(str::trim)
620}
621
622/// If `line` declares a type-namespace item — `mod`/`struct`/`enum`/`trait`/`type`/
623/// `union`, optionally `pub`-qualified, optionally `unsafe` (traits) — record its
624/// name. Field/variable positions can't start a trimmed line with these keywords, so
625/// a leading-keyword scan is enough for rustfmt-shaped code.
626fn collect_declared_type_name(line: &str, out: &mut BTreeSet<String>) {
627    let mut t = line.trim();
628    if let Some(rest) = t.strip_prefix("pub") {
629        let rest = rest.trim_start();
630        t = if let Some(after_paren) = rest.strip_prefix('(') {
631            match after_paren.split_once(')') {
632                Some((_, after)) => after.trim_start(),
633                None => return,
634            }
635        } else {
636            rest
637        };
638    }
639    if let Some(rest) = t.strip_prefix("unsafe ") {
640        t = rest.trim_start();
641    }
642    for kw in ["mod ", "struct ", "enum ", "trait ", "type ", "union "] {
643        if let Some(rest) = t.strip_prefix(kw) {
644            let rest = rest.trim_start();
645            let end = rest
646                .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
647                .unwrap_or(rest.len());
648            if end > 0 {
649                out.insert(rest[..end].to_string());
650            }
651            return;
652        }
653    }
654}
655
656/// Record the name(s) a `use` path binds into `facts` — `path::to::name`,
657/// `path as alias`, and brace groups (`std::{fs, path::PathBuf}`, nested one level
658/// per recursion). `prefix` is the already-consumed leading path (empty at the top).
659fn collect_use_bindings(prefix: &str, entry: &str, facts: &mut FsImportFacts) {
660    let entry = entry.trim();
661    if entry.is_empty() {
662        return;
663    }
664    if let Some((path_part, group)) = entry.split_once('{') {
665        let inner_prefix = join_use_path(prefix, path_part.trim().trim_end_matches("::"));
666        let group = group.strip_suffix('}').unwrap_or(group);
667        for part in split_group_entries(group) {
668            collect_use_bindings(&inner_prefix, part, facts);
669        }
670        return;
671    }
672    let (path_part, alias) = match entry.split_once(" as ") {
673        Some((p, a)) => (p.trim(), Some(a.trim())),
674        None => (entry, None),
675    };
676    let full = join_use_path(prefix, path_part);
677    // `use std::fs::{self};` binds `fs` — normalise the `self` leaf away.
678    let full = full.strip_suffix("::self").unwrap_or(&full);
679    let last = full.rsplit("::").next().unwrap_or(full);
680    let name = alias.unwrap_or(last);
681    if name.is_empty() || name == "*" {
682        return; // globs bind no single name; `super::*` is tracked separately
683    }
684    facts.use_bound_names.insert(name.to_string());
685    if full == "std::fs" || full.starts_with("std::fs::") {
686        facts.std_fs_bindings.insert(name.to_string());
687    }
688}
689
690fn join_use_path(prefix: &str, part: &str) -> String {
691    if prefix.is_empty() {
692        part.to_string()
693    } else {
694        format!("{prefix}::{part}")
695    }
696}
697
698/// Split a brace group's contents on top-level commas only — `fs::{self, File}, io`
699/// is two entries, not three.
700fn split_group_entries(s: &str) -> Vec<&str> {
701    let mut out = Vec::new();
702    let mut depth = 0i32;
703    let mut start = 0;
704    for (i, c) in s.char_indices() {
705        match c {
706            '{' => depth += 1,
707            '}' => depth -= 1,
708            ',' if depth == 0 => {
709                out.push(&s[start..i]);
710                start = i + 1;
711            }
712            _ => {}
713        }
714    }
715    out.push(&s[start..]);
716    out
717}
718
719/// Every identifier `NAME` occurring as `NAME::` where the character before `NAME` is
720/// not `:` — i.e. a path *root*, so `std::fs::read` contributes `std`, never `fs`.
721/// Same line discipline as the text scan: comments included, production scope only
722/// (the caller has already excluded test ranges).
723fn collect_bare_path_roots(line: &str, out: &mut BTreeSet<String>) {
724    let bytes = line.as_bytes();
725    let mut search_from = 0;
726    while let Some(rel) = line[search_from..].find("::") {
727        let pos = search_from + rel;
728        let mut start = pos;
729        while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') {
730            start -= 1;
731        }
732        if start < pos && (start == 0 || bytes[start - 1] != b':') {
733            out.insert(line[start..pos].to_string());
734        }
735        search_from = pos + 2;
736    }
737}
738
739/// Every `super::`/`self::`/`crate::`-rooted path on `line`, as its segment chain —
740/// `super::fs::read_to_string(p)` yields `["super", "fs", "read_to_string"]`. The
741/// root must sit at a bare word boundary (not `a_super::` or `a::super::`), so only
742/// genuine path roots are collected; `Self::` (capital) never matches, and `self.x`
743/// has no `::` to match.
744fn collect_qualified_paths(line: &str, out: &mut BTreeSet<Vec<String>>) {
745    let bytes = line.as_bytes();
746    for root in ["super", "self", "crate"] {
747        let mut from = 0;
748        while let Some(rel) = line[from..].find(root) {
749            let start = from + rel;
750            let root_end = start + root.len();
751            from = root_end;
752            let boundary_ok = start == 0 || {
753                let c = bytes[start - 1];
754                !(c.is_ascii_alphanumeric() || c == b'_' || c == b':')
755            };
756            if !boundary_ok || !line[root_end..].starts_with("::") {
757                continue;
758            }
759            let mut segments = vec![root.to_string()];
760            let mut pos = root_end;
761            while line[pos..].starts_with("::") {
762                let seg_start = pos + 2;
763                let mut seg_end = seg_start;
764                while seg_end < bytes.len()
765                    && (bytes[seg_end].is_ascii_alphanumeric() || bytes[seg_end] == b'_')
766                {
767                    seg_end += 1;
768                }
769                if seg_end == seg_start {
770                    break; // `super::*` and friends — no further identifier
771                }
772                segments.push(line[seg_start..seg_end].to_string());
773                pos = seg_end;
774            }
775            if segments.len() >= 2 {
776                out.insert(segments);
777            }
778        }
779    }
780}
781
782/// The file defining `path`'s parent module, by the standard layout: `a/b.rs`'s parent
783/// is `a.rs` (or `a/mod.rs`), `a/mod.rs`'s parent is the crate root, and the roots
784/// (`lib.rs`/`main.rs`) have none. `#[path]`-remapped modules are not handled — none
785/// exist below the driver, and a text-level probe can't chase them anyway.
786fn module_parent(path: &Path, files: &[(PathBuf, String)]) -> Option<usize> {
787    let stem = path.file_stem()?.to_str()?;
788    let dir = path.parent().filter(|d| !d.as_os_str().is_empty());
789    let parent_module: PathBuf = if stem == "mod" {
790        dir?.parent().map(Path::to_path_buf).unwrap_or_default()
791    } else if let Some(dir) = dir {
792        dir.to_path_buf()
793    } else {
794        if stem == "lib" || stem == "main" {
795            return None;
796        }
797        PathBuf::new()
798    };
799    let candidates = if parent_module.as_os_str().is_empty() {
800        vec![PathBuf::from("lib.rs"), PathBuf::from("main.rs")]
801    } else {
802        vec![
803            parent_module.with_extension("rs"),
804            parent_module.join("mod.rs"),
805        ]
806    };
807    candidates
808        .iter()
809        .find_map(|c| files.iter().position(|(p, _)| p == c))
810}
811
812/// The scopes whose bindings a name used in module `i` can see: the module itself,
813/// then each ancestor reachable while every module below it glob-imports `super::*`.
814fn visible_scopes(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> Vec<usize> {
815    let mut scopes = vec![i];
816    let mut cur = i;
817    loop {
818        if !facts[cur].glob_imports_super {
819            break;
820        }
821        let Some(parent) = parents[cur] else { break };
822        scopes.push(parent);
823        cur = parent;
824    }
825    scopes
826}
827
828/// How `name` resolves in module `m`'s namespace, walking [`visible_scopes`] with
829/// nearest binding winning — a closer non-`std::fs` `use` binding *or* locally
830/// declared type-namespace item shadows a farther `std::fs` binding, as in Rust.
831enum NameResolution {
832    StdFs,
833    Other,
834    Unbound,
835}
836
837fn resolve_name_in_module(
838    m: usize,
839    name: &str,
840    facts: &[FsImportFacts],
841    parents: &[Option<usize>],
842) -> NameResolution {
843    for s in visible_scopes(m, facts, parents) {
844        if facts[s].std_fs_bindings.contains(name) {
845            return NameResolution::StdFs;
846        }
847        if facts[s].use_bound_names.contains(name) || facts[s].declared_type_names.contains(name) {
848            return NameResolution::Other;
849        }
850    }
851    NameResolution::Unbound
852}
853
854/// Does a bare path root in file `i` resolve to `std::fs` through the bindings it
855/// can see? Candidates are the names any visible scope binds to `std::fs`; each is
856/// then resolved from `i` with nearest-binding-wins shadowing.
857fn resolves_bare_std_fs(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> bool {
858    let scopes = visible_scopes(i, facts, parents);
859    let mut candidates: BTreeSet<&str> = BTreeSet::new();
860    for &s in &scopes {
861        candidates.extend(facts[s].std_fs_bindings.iter().map(String::as_str));
862    }
863    candidates.into_iter().any(|name| {
864        facts[i].bare_path_roots.contains(name)
865            && matches!(
866                resolve_name_in_module(i, name, facts, parents),
867                NameResolution::StdFs
868            )
869    })
870}
871
872/// Does a `super::`/`self::`/`crate::`-qualified path in file `i` reach a `std::fs`
873/// binding (#1016 review)? Unlike the bare-root case these need no glob import: the
874/// root picks the starting module directly (`super`-hops up the parent chain, `self`
875/// the file itself, `crate` the crate root), then each further segment either
876/// resolves in that module's namespace — `std::fs` flags, anything else stops — or
877/// descends into a child module file and continues. Inline `mod name { … }` blocks
878/// are not modelled (their `use` bindings live in the same file, which the text scan
879/// and bare-root resolution already cover).
880fn resolves_qualified_std_fs(
881    i: usize,
882    facts: &[FsImportFacts],
883    parents: &[Option<usize>],
884    files: &[(PathBuf, String)],
885) -> bool {
886    facts[i]
887        .qualified_paths
888        .iter()
889        .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
890}
891
892fn qualified_chain_reaches_std_fs(
893    chain: &[String],
894    i: usize,
895    facts: &[FsImportFacts],
896    parents: &[Option<usize>],
897    files: &[(PathBuf, String)],
898) -> bool {
899    let mut idx = 1;
900    let mut m = match chain[0].as_str() {
901        "self" => i,
902        "crate" => {
903            let root = files
904                .iter()
905                .position(|(p, _)| p == Path::new("lib.rs") || p == Path::new("main.rs"));
906            match root {
907                Some(root) => root,
908                None => return false,
909            }
910        }
911        "super" => {
912            let mut m = i;
913            idx = 0;
914            while idx < chain.len() && chain[idx] == "super" {
915                let Some(parent) = parents[m] else {
916                    return false;
917                };
918                m = parent;
919                idx += 1;
920            }
921            m
922        }
923        _ => return false,
924    };
925    while idx < chain.len() {
926        let seg = chain[idx].as_str();
927        // Resolve `seg` in `m`, nearest scope first. Within a scope, a child module
928        // file for `seg` is checked *before* the shadow set: a declared `mod seg;`
929        // lands `seg` in `declared_type_names`, but that declaration IS the child
930        // module — it's the path's next hop, not a shadow over it. (In valid Rust a
931        // module and another same-name type-namespace item can't coexist in one
932        // scope, so the ordering costs nothing.)
933        let mut next = None;
934        for s in visible_scopes(m, facts, parents) {
935            if facts[s].std_fs_bindings.contains(seg) {
936                return true;
937            }
938            if let Some(child) = child_module_file(s, seg, files) {
939                next = Some(child);
940                break;
941            }
942            if facts[s].use_bound_names.contains(seg) || facts[s].declared_type_names.contains(seg)
943            {
944                return false; // bound to something that is neither std::fs nor a module
945            }
946        }
947        let Some(child) = next else {
948            return false;
949        };
950        m = child;
951        idx += 1;
952    }
953    false
954}
955
956/// The file defining module `m`'s child module `seg`, if it exists as a file:
957/// `lib.rs` + `a` → `a.rs`/`a/mod.rs`, `a.rs` + `b` → `a/b.rs`/`a/b/mod.rs`,
958/// `a/mod.rs` + `b` → `a/b.rs`/`a/b/mod.rs`.
959fn child_module_file(m: usize, seg: &str, files: &[(PathBuf, String)]) -> Option<usize> {
960    let m_path = &files[m].0;
961    let module_dir: PathBuf = match m_path.file_stem().and_then(|s| s.to_str()) {
962        Some("mod") => m_path.parent().unwrap_or(Path::new("")).to_path_buf(),
963        Some("lib") | Some("main") if m_path.parent().is_none_or(|p| p.as_os_str().is_empty()) => {
964            PathBuf::new()
965        }
966        _ => m_path.with_extension(""),
967    };
968    let candidates = [
969        module_dir.join(format!("{seg}.rs")),
970        module_dir.join(seg).join("mod.rs"),
971    ];
972    candidates
973        .iter()
974        .find_map(|c| files.iter().position(|(p, _)| p == c))
975}
976
977/// Every `#[cfg(test)] mod <ident> { ... }` block in `lines`, as inclusive
978/// `(start_line, end_line)` line-index ranges — every occurrence, not just a single
979/// trailing block. A file in this codebase can carry several test modules scattered
980/// through it with production code between them — `bynk-emit/src/emitter/lower.rs` has
981/// two, 1031 lines apart, and treating "everything after the first (or last)
982/// `#[cfg(test)]`" as one cutoff silently misclassifies that intervening production
983/// code as test-scope (caught in review: it made `fs_below_driver`, a *gated* probe,
984/// blind over that span, and inflated `test_density`'s ratio by up to 39%).
985///
986/// A block's end is found by real brace-depth counting via [`brace_delta`], not a
987/// "first column-0 `}`" shortcut: an earlier version of this fix tried exactly that
988/// shortcut (reasoning that rustfmt always dedents a closing brace back to column 0)
989/// and it broke on files like `bynk-ide/src/sequence.rs`, whose test module embeds
990/// multi-line `.bynk`/TypeScript fixture source as string literals — source that
991/// itself contains a column-0 `}` closing a top-level construct *inside the string*,
992/// which the shortcut mistook for the end of the Rust `mod` block, truncating it by
993/// hundreds of lines. `brace_delta` skips characters inside Rust string/char literals
994/// and comments, so embedded fixture text can't be mistaken for real Rust braces.
995///
996/// Only matches a brace-opening `mod` line — `#[cfg(test)] mod foo;` (an external-file
997/// declaration, not an inline scope) does not open a range.
998fn test_mod_ranges(lines: &[&str]) -> Vec<(usize, usize)> {
999    let mut ranges = Vec::new();
1000    let mut i = 0;
1001    while i < lines.len() {
1002        if lines[i].trim() == "#[cfg(test)]"
1003            && let Some(off) = lines[i + 1..].iter().position(|l| !l.trim().is_empty())
1004        {
1005            let mod_line = i + 1 + off;
1006            let t = lines[mod_line].trim();
1007            if t.starts_with("mod ") && t.ends_with('{') {
1008                let mut depth = 0i32;
1009                let mut state = BraceScanState::Normal;
1010                let mut started = false;
1011                let mut end = lines.len() - 1;
1012                for (j, line) in lines[mod_line..].iter().enumerate() {
1013                    let (delta, new_state) = brace_delta(line, state);
1014                    state = new_state;
1015                    depth += delta;
1016                    if depth != 0 {
1017                        started = true;
1018                    }
1019                    if started && depth == 0 {
1020                        end = mod_line + j;
1021                        break;
1022                    }
1023                }
1024                ranges.push((mod_line, end));
1025                i = end + 1;
1026                continue;
1027            }
1028        }
1029        i += 1;
1030    }
1031    ranges
1032}
1033
1034fn in_test_range(line_idx: usize, ranges: &[(usize, usize)]) -> bool {
1035    ranges
1036        .iter()
1037        .any(|(start, end)| line_idx >= *start && line_idx <= *end)
1038}
1039
1040/// Scanner state carried across lines for [`brace_delta`]: whether the cursor is
1041/// inside a string literal, a raw string (with its `#`-count), or a block comment
1042/// (with nesting depth — Rust block comments nest).
1043#[derive(Clone, Copy, PartialEq)]
1044enum BraceScanState {
1045    Normal,
1046    InString,
1047    InRawString(u8),
1048    InBlockComment(u32),
1049}
1050
1051/// The net `{`/`}` depth change in `line`, skipping characters inside Rust string/char
1052/// literals, raw strings, and line/block comments — a naive per-character brace count
1053/// breaks the moment a line contains a fixture string like `"fn f() { \"{\" }"` or a
1054/// doc comment mentioning a brace. Returns the depth delta and the state to carry into
1055/// the next line (a string or block comment can span line boundaries).
1056fn brace_delta(line: &str, mut state: BraceScanState) -> (i32, BraceScanState) {
1057    let mut delta = 0i32;
1058    let chars: Vec<char> = line.chars().collect();
1059    let mut i = 0;
1060    while i < chars.len() {
1061        match state {
1062            BraceScanState::Normal => {
1063                if chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
1064                    break; // rest of the line is a line comment
1065                }
1066                if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1067                    state = BraceScanState::InBlockComment(1);
1068                    i += 2;
1069                    continue;
1070                }
1071                if chars[i] == '"' {
1072                    state = BraceScanState::InString;
1073                    i += 1;
1074                    continue;
1075                }
1076                if chars[i] == 'r' && matches!(chars.get(i + 1), Some('"') | Some('#')) {
1077                    let mut j = i + 1;
1078                    let mut hashes = 0u8;
1079                    while chars.get(j) == Some(&'#') {
1080                        hashes += 1;
1081                        j += 1;
1082                    }
1083                    if chars.get(j) == Some(&'"') {
1084                        state = BraceScanState::InRawString(hashes);
1085                        i = j + 1;
1086                        continue;
1087                    }
1088                }
1089                if chars[i] == '\'' {
1090                    // A `'\x'`/`'\\'`-style escaped char literal, or a plain `'x'` —
1091                    // skip past it so its contents can't be mistaken for braces.
1092                    // Anything else (no closing `'` within a couple of chars) is a
1093                    // lifetime, which owns no closing quote to skip.
1094                    if chars.get(i + 1) == Some(&'\\') {
1095                        let mut j = i + 2;
1096                        while j < chars.len() && chars[j] != '\'' {
1097                            j += 1;
1098                        }
1099                        i = (j + 1).min(chars.len());
1100                        continue;
1101                    } else if chars.get(i + 2) == Some(&'\'') {
1102                        i += 3;
1103                        continue;
1104                    }
1105                }
1106                match chars[i] {
1107                    '{' => delta += 1,
1108                    '}' => delta -= 1,
1109                    _ => {}
1110                }
1111                i += 1;
1112            }
1113            BraceScanState::InString => {
1114                if chars[i] == '\\' {
1115                    i += 2;
1116                    continue;
1117                }
1118                if chars[i] == '"' {
1119                    state = BraceScanState::Normal;
1120                }
1121                i += 1;
1122            }
1123            BraceScanState::InRawString(hashes) => {
1124                if chars[i] == '"' {
1125                    let mut j = i + 1;
1126                    let mut h = 0u8;
1127                    while chars.get(j) == Some(&'#') && h < hashes {
1128                        h += 1;
1129                        j += 1;
1130                    }
1131                    if h == hashes {
1132                        state = BraceScanState::Normal;
1133                        i = j;
1134                        continue;
1135                    }
1136                }
1137                i += 1;
1138            }
1139            BraceScanState::InBlockComment(depth) => {
1140                if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1141                    state = BraceScanState::InBlockComment(depth + 1);
1142                    i += 2;
1143                    continue;
1144                }
1145                if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
1146                    state = if depth <= 1 {
1147                        BraceScanState::Normal
1148                    } else {
1149                        BraceScanState::InBlockComment(depth - 1)
1150                    };
1151                    i += 2;
1152                    continue;
1153                }
1154                i += 1;
1155            }
1156        }
1157    }
1158    (delta, state)
1159}
1160
1161// --- Gated probe 3: options_sources --------------------------------------
1162
1163/// R2.3. `CompileOptions` (in `bynk-emit/src/project.rs`) has a `sources` field.
1164fn options_sources(root: &Path) -> Probe {
1165    let src = std::fs::read_to_string(root.join("bynk-emit/src/project.rs")).unwrap_or_default();
1166    let present = struct_body(&src, "CompileOptions").is_some_and(|body| body.contains("sources"));
1167    Probe {
1168        name: "options_sources",
1169        gated: true,
1170        reads: if present {
1171            "present".to_string()
1172        } else {
1173            "absent".to_string()
1174        },
1175    }
1176}
1177
1178/// The `{ ... }` body text of `struct <name>` in `src`, brace-matched from the struct's
1179/// own opening brace to its close.
1180fn struct_body<'a>(src: &'a str, name: &str) -> Option<&'a str> {
1181    let needle = format!("struct {name}");
1182    let start = src.find(&needle)?;
1183    let open = start + src[start..].find('{')?;
1184    let mut depth = 0i32;
1185    for (offset, ch) in src[open..].char_indices() {
1186        match ch {
1187            '{' => depth += 1,
1188            '}' => {
1189                depth -= 1;
1190                if depth == 0 {
1191                    return Some(&src[open..open + offset + 1]);
1192                }
1193            }
1194            _ => {}
1195        }
1196    }
1197    None
1198}
1199
1200// --- Gated probe 4: hoist_sinks -------------------------------------------
1201
1202/// R6.2. Live (non-comment) occurrences of the sink-passing signature
1203/// `stmts: &mut Vec<String>` in `bynk-emit`. Tier B (T2.1) deletes it entirely.
1204fn hoist_sinks(root: &Path) -> Probe {
1205    let dir = root.join("bynk-emit/src");
1206    let needle = "stmts: &mut Vec<String>";
1207    let mut count = 0usize;
1208    for (_, contents) in rust_files(&dir) {
1209        for line in contents.lines() {
1210            if !is_line_comment(line) && line.contains(needle) {
1211                count += 1;
1212            }
1213        }
1214    }
1215    Probe {
1216        name: "hoist_sinks",
1217        gated: true,
1218        reads: count.to_string(),
1219    }
1220}
1221
1222// --- Gated probe 5: span_keyed_maps ---------------------------------------
1223
1224/// R2.4. Whole-repo occurrences of `HashMap<Span` (comments included — the phase-3
1225/// migration target is every mention, not just live call sites), **excluding
1226/// `xtask` itself**: this probe's own doc comment and source both name the search
1227/// string, which would otherwise self-count every time this file is touched — the
1228/// same self-reference hazard flagged for the dead-identifier probes below, caught
1229/// here by running the probe against itself before committing the first table.
1230fn span_keyed_maps(root: &Path) -> Probe {
1231    let count = count_repo_wide(root, "HashMap<Span", &["xtask"]);
1232    Probe {
1233        name: "span_keyed_maps",
1234        gated: true,
1235        reads: count.to_string(),
1236    }
1237}
1238
1239fn count_repo_wide(root: &Path, needle: &str, exclude_crates: &[&str]) -> usize {
1240    let mut total = 0usize;
1241    for entry in top_level_crate_dirs(root) {
1242        if exclude_crates
1243            .iter()
1244            .any(|c| entry.file_name().is_some_and(|n| n == *c))
1245        {
1246            continue;
1247        }
1248        for (_, contents) in rust_files(&entry.join("src")) {
1249            total += contents.matches(needle).count();
1250        }
1251    }
1252    total
1253}
1254
1255/// Every workspace member crate directory (anything at the repo root with a
1256/// `Cargo.toml` and a `src/` dir), excluding `target` and non-crate directories.
1257fn top_level_crate_dirs(root: &Path) -> Vec<PathBuf> {
1258    let mut out = Vec::new();
1259    let Ok(entries) = std::fs::read_dir(root) else {
1260        return out;
1261    };
1262    for entry in entries.flatten() {
1263        let path = entry.path();
1264        if path.is_dir() && path.join("Cargo.toml").is_file() && path.join("src").is_dir() {
1265            out.push(path);
1266        }
1267    }
1268    out.sort();
1269    out
1270}
1271
1272// --- Gated probe 6: emit_diagnostics --------------------------------------
1273
1274/// R3.5. `bynk.*` string literals in `bynk-emit`/`bynk-check` source, cross-referenced
1275/// against `bynk_syntax::diagnostics::REGISTRY` — not pattern-matched. A literal not in
1276/// `REGISTRY` is a commons/namespace path (e.g. `bynk.locale`, the compiled first-party
1277/// source module name), not a diagnostic code, and must not inflate the count (#999
1278/// Decision A: this cross-reference is what makes the exclusion correct by
1279/// construction rather than a second hand-maintained list).
1280fn emit_diagnostics(root: &Path) -> Probe {
1281    let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
1282        .iter()
1283        .map(|d| d.code)
1284        .collect();
1285    let mut parts = Vec::new();
1286    for (label, dir) in [
1287        ("bynk-emit", "bynk-emit/src"),
1288        ("bynk-check", "bynk-check/src"),
1289    ] {
1290        let mut naive: BTreeSet<String> = BTreeSet::new();
1291        for (_, contents) in rust_files(&root.join(dir)) {
1292            for lit in bynk_dotted_literals(&contents) {
1293                naive.insert(lit.to_string());
1294            }
1295        }
1296        let true_count = naive
1297            .iter()
1298            .filter(|l| registry.contains(l.as_str()))
1299            .count();
1300        parts.push(format!("{label}={true_count}/{}", naive.len()));
1301    }
1302    Probe {
1303        name: "emit_diagnostics",
1304        gated: true,
1305        reads: format!("{} (true/naive)", parts.join(", ")),
1306    }
1307}
1308
1309// --- Gated probe 7: ide_emit_edge -----------------------------------------
1310
1311/// R10.2. `bynk-ide` → `bynk-emit` in the manifest (`bynk-emit.workspace = true` or an
1312/// equivalent path/version dependency line).
1313fn ide_emit_edge(root: &Path) -> Probe {
1314    let manifest = std::fs::read_to_string(root.join("bynk-ide/Cargo.toml")).unwrap_or_default();
1315    let present = manifest
1316        .lines()
1317        .any(|l| l.trim_start().starts_with("bynk-emit"));
1318    Probe {
1319        name: "ide_emit_edge",
1320        gated: true,
1321        reads: if present {
1322            "present".to_string()
1323        } else {
1324            "absent".to_string()
1325        },
1326    }
1327}
1328
1329// --- Gated probe 8: ast_importers -----------------------------------------
1330
1331/// #1176: `bynk-emit::ir`'s own two files — named exactly, not by path prefix, the same
1332/// permanent-carve-out discipline [`NAMED_FS_EXCEPTIONS`] and [`emit_diagnostics`]'s
1333/// registry cross-reference already use. An `Ast → Ir` lowering pass importing
1334/// `bynk_syntax::ast` is that pass's entire job, not the AST-walking this track is
1335/// closing (phase 6's own P6.9 correction, #1167 — see the retired `the-ir.md`'s
1336/// closing summary, `design/archive/retired-tracks.md`) — but `project.rs` also
1337/// imports `bynk_syntax::ast` today (`EmitProjectCtx` holding `ActorDecl`/`AgentDecl`
1338/// fields directly), and that *is* exactly the still-open R6.13 defect this probe
1339/// tracks (P6.6: "closes the emitter reading AST declarations directly"). A
1340/// path-prefix rule scoped to `emitter/**` would exclude that file right along with
1341/// `ir/`'s legitimate ones, silently undercounting real remaining work — see
1342/// [`is_named_ast_importer`].
1343///
1344/// #1184 review: this exclusion is necessary but not sufficient for R6.13. `ir.rs`
1345/// itself still holds several AST types directly in `IrItem`-adjacent struct fields
1346/// (`Arc<TypeDecl>`, `Arc<FnDecl>`, `HandlerKind`, `Refinement`, `SchemaVersionPattern`)
1347/// rather than IR-native equivalents — an emitter reading e.g. `IrHandler::kind`, which
1348/// *is* `ast::HandlerKind`, touches the AST without ever spelling `bynk_syntax::ast`
1349/// itself, so it is invisible to this probe by construction. `ast_importers` reading
1350/// its retired floor (5 — `design/archive/retired-tracks.md`'s own closing summary
1351/// has the per-file argument) proves no *remaining* file outside these two and the
1352/// five-file rendering subtree imports the AST module directly; it does not by
1353/// itself prove every `IrItem` field is AST-free.
1354///
1355/// #1187's own closing scoping pass adds one more, on different grounds than the
1356/// `ir.rs`/`ir/lower.rs` pair above: `project/tests_emit.rs` was deliberately *not*
1357/// added alongside `project.rs` when this list was first cut (the
1358/// `ast_importer_exclusion_is_named_not_prefixed` test below used to assert exactly
1359/// that) — #1187's own scoping pass found new evidence changing that: its test/suite
1360/// case bodies call `emitter::lower_block_to_async_body`/`lower_test_case_body`/
1361/// `lower_integration_case_body` directly (the Q7-settled body-rendering pass —
1362/// `emitter/lower.rs` keeps hand-writing TypeScript source text after phase 6's
1363/// cutover, the printer that would change that is phase 7's), and
1364/// its own `driver_param_ty`/`strip_effect_httpresult` read a handler's *declared*
1365/// param/return `TypeRef` with no corresponding `TyId` available at that call site
1366/// (the same caller-reads-callee's-raw-declared-shape pattern #661 established for
1367/// cross-context codec generation). Both are the Q7/printer kind of unreachable, not
1368/// the "still open, real work" kind the original exclusion list deliberately left this
1369/// file out of — the correction is new evidence, not a reversal of that reasoning.
1370///
1371/// Review of #1210: `emitter.rs`/`emitter/lower.rs` themselves were considered for
1372/// this same exclusion and **rejected** — Q7 settles that these files' *body-rendering*
1373/// surface stays AST-parameter-driven, but both files also hold live, currently
1374/// untouched AST-*declaration* reads with no such gate: `emitter.rs`'s own
1375/// `CommonsItem::Service`/`svc.protocol` walk (consumed-event-root collection) and
1376/// `emitter/lower.rs`'s own `cap_op_param_names` (`CommonsItem::Capability`/`c.ops`)
1377/// were exactly the P6.2/P6.6-class conversions phase 6's own slice decomposition
1378/// still listed as in scope at the time, not body-rendering. Excluding either file
1379/// would have hidden that real, fixable surface from this probe the same way a
1380/// path-prefix rule would — the harm the named-not-prefixed discipline above exists
1381/// to prevent, just at file granularity instead of directory granularity. (Both
1382/// converted their own reachable surface later, without joining this list — phase 6's
1383/// closing summary, `design/archive/retired-tracks.md`, has the account.)
1384///
1385/// P6.33 (phase 6's own §6a.D re-settling, 19 August 2026): `emitter/serialisation.rs`
1386/// joins the list, on grounds distinct from every entry above — not Q7 body-rendering,
1387/// not test-only reach, but a phase boundary. Unlike `emitter.rs`/`emitter/lower.rs`,
1388/// this file holds no `CommonsItem`-declaration-read surface at all (confirmed:
1389/// `grep -c bynk_syntax::ast` finds only its one `use` line and its `#[cfg(test)]`
1390/// module) — its entire AST surface, ~120 sites, *is* the `TypeRef`-driven JSON/wire
1391/// codec renderer (`emit_record_codec`/`emit_sum_codec`/`serialise_expr`/
1392/// `deserialise_expr`/`ts_inner_type` and siblings). Rendering a checker type as TS
1393/// codec source text is the same class of question Q7 already settled belongs to the
1394/// eventual printer (phase 7, `bynk-ts`) — this file has no `use crate::ir` at all, so
1395/// nothing here has been resisting an available IR-native alternative; none exists. The
1396/// re-settling found no clean way to shrink this file's AST surface further without
1397/// building printer infrastructure phase 6's own scope already excluded.
1398///
1399/// P6.49 (phase 6's own §6b, 19 August 2026): `project.rs` — R6.13's own still-open
1400/// declaration-read surface named at the top of this doc block, above — cleared
1401/// **without joining this list**. Nine slices (P6.42–P6.49) relocated its remaining
1402/// declaration reads to the `bynk-check`/`bynk-project` crates that already own the
1403/// data (`SourceUnit::name()`, `own_contract_hashes`, `discover_event_subscribers`,
1404/// `combined_types_for_unit_info`, two owner-side accessors,
1405/// `lower_event_subscriber_shapes_ir`, `walk_unit_table_bodies`) or re-exported a type
1406/// from a `bynk-check` module whose own public API was already parameterised by it
1407/// (`TypeDecl`/`FnDecl`/`Visibility` from `project_model`, `ActorDecl` from `actors` —
1408/// the P6.27 `ExprId` precedent, applied four more times). This is the evidence this
1409/// exclusion list's own entries above are real, earned exclusions and not a standing
1410/// habit: the harder file cleared its own way, on its own schedule, with zero new
1411/// entries here.
1412///
1413/// **P6.58/P6.59, 19 August 2026: phase 6 (`the-ir.md`, spine #1137) retired at this
1414/// probe reading 5, not 0.** The floor is exactly `bynk-emit/src/emitter{,/**}` —
1415/// `emitter.rs`, `emitter/emit.rs`, `emitter/lower.rs`, `emitter/workers.rs`,
1416/// `emitter/workers_entry.rs` — the TypeScript-rendering subtree phase 7's own printer
1417/// inherits; each file's own structural reason, and the full slice history behind
1418/// every correction this doc block narrates, live in `design/archive/retired-tracks.md`
1419/// now that `the-ir.md` itself is gone. This exclusion list does **not** grow to
1420/// reach that floor — the floor is a fact about `AST_IMPORTER_EXCEPTIONS`'s own
1421/// four entries staying exactly these four, not a fifth argument for adding to them.
1422/// The probe itself stays gated, unchanged, reading 5: a regression ratchet phase 7
1423/// inherits and drives down as it builds the printer this floor's own residue names.
1424///
1425/// **Arc D, P7.12 (crate carve): `ir.rs`/`ir/lower.rs` drop out of this list
1426/// entirely — not because they stopped importing the AST (unchanged, still
1427/// do), but because they left `bynk-emit/src` altogether, carved into the new
1428/// `bynk-ir`/`bynk-lower` crates ADR 0332 deferred and ADR 0385 triggered.**
1429/// This probe was never scoped to those crates (`ast_importer_files` walks
1430/// `bynk-emit/src` only), so the pair is simply outside its universe now,
1431/// the same way a file moving to `bynk-check`/`bynk-project` already leaves
1432/// silently rather than needing its own exclusion-list removal step. Two
1433/// named exclusions remain.
1434const AST_IMPORTER_EXCEPTIONS: &[&str] = &["project/tests_emit.rs", "emitter/serialisation.rs"];
1435
1436/// Is `rel_path` (relative to `bynk-emit/src`) one of [`AST_IMPORTER_EXCEPTIONS`]?
1437fn is_named_ast_importer(rel_path: &Path) -> bool {
1438    let rel = rel_path.to_string_lossy().replace('\\', "/");
1439    AST_IMPORTER_EXCEPTIONS.contains(&rel.as_str())
1440}
1441
1442/// Is `contents` a module (not a nested block) that glob-imports its parent —
1443/// i.e. does it carry a top-level (column-0) `use super::*;`? Rust's own privacy
1444/// rule makes a parent module's private `use` visible to descendant modules, so a
1445/// file matching this can expose `bynk_syntax::ast` names it never spells itself
1446/// (P6.26 review, #1259) — deliberately column-0 only, so a `use super::*;`
1447/// *inside* a nested `#[cfg(test)] mod tests { .. }` block (glob-importing its own
1448/// immediately-enclosing module, not the grandparent file on disk) doesn't
1449/// false-positive this check.
1450fn has_module_level_super_glob(contents: &str) -> bool {
1451    contents.lines().any(|line| line == "use super::*;")
1452}
1453
1454/// For `rel_path` = `<dir>/<file>.rs`, does the sibling module file `<dir>.rs`
1455/// (the parent module a top-level `use super::*;` in `rel_path` would inherit
1456/// from) itself contain `bynk_syntax::ast`? `None` if `rel_path` has no such
1457/// parent (a file directly under `bynk-emit/src`, e.g. `emitter.rs` itself).
1458fn super_glob_parent_imports_ast(dir: &Path, rel_path: &Path) -> Option<bool> {
1459    let parent_dir = rel_path.parent()?;
1460    if parent_dir.as_os_str().is_empty() {
1461        return None;
1462    }
1463    let parent_file = dir.join(parent_dir).with_extension("rs");
1464    Some(
1465        std::fs::read_to_string(&parent_file)
1466            .is_ok_and(|contents| contents.contains("bynk_syntax::ast")),
1467    )
1468}
1469
1470/// The files [`ast_importers`] counts: `bynk-emit/src` files whose contents match
1471/// `bynk_syntax::ast` **or** that inherit it from an AST-importing parent through a
1472/// top-level `use super::*;` (P6.26 review, #1259 — a file that stops spelling the
1473/// AST module directly by deleting its own explicit import, while a live `use
1474/// super::*;` still channels a still-AST-importing parent's names in, must stay
1475/// counted; otherwise a future partial conversion could silently drop this probe
1476/// without the underlying AST dependency actually being gone), excluding
1477/// [`AST_IMPORTER_EXCEPTIONS`]. Split out from [`ast_importers`] so a test can
1478/// assert on the actual survivor set, not just its length (#1184 review).
1479fn ast_importer_files(root: &Path) -> Vec<PathBuf> {
1480    let dir = root.join("bynk-emit/src");
1481    rust_files(&dir)
1482        .into_iter()
1483        .filter(|(path, contents)| {
1484            contents.contains("bynk_syntax::ast")
1485                || (has_module_level_super_glob(contents)
1486                    && super_glob_parent_imports_ast(&dir, path.strip_prefix(&dir).unwrap_or(path))
1487                        .unwrap_or(false))
1488        })
1489        .filter(|(path, _)| !is_named_ast_importer(path.strip_prefix(&dir).unwrap_or(path)))
1490        .map(|(path, _)| path)
1491        .collect()
1492}
1493
1494/// R6.13. Files in `bynk-emit/src` that import `bynk_syntax::ast`, excluding
1495/// [`AST_IMPORTER_EXCEPTIONS`] — phase 6's own remaining AST import surface (retired,
1496/// spine #1137; `design/archive/retired-tracks.md` has the closing summary). #1176:
1497/// the unexcluded, crate-wide count could never reach 0 while `bynk-emit::ir`'s
1498/// lowering pass exists at all; this exclusion is what let the probe track phase 6's
1499/// real completion criterion instead of a floor its own IR module structurally could
1500/// not clear. Gated at 5, phase 6's own retired floor, for phase 7 to drive down.
1501fn ast_importers(root: &Path) -> Probe {
1502    Probe {
1503        name: "ast_importers",
1504        gated: true,
1505        reads: ast_importer_files(root).len().to_string(),
1506    }
1507}
1508
1509// --- Gated probe 9: emit_abi_shapes ---------------------------------------
1510
1511/// ADR 0310 D1's four emit-ABI shapes, as they surface as import names in the vendored
1512/// bindings — the `Result`/`Option` tag layout plus `JsonError`, `Uuid`, `FetchError`.
1513const EMIT_ABI: &[&str] = &[
1514    "Result",
1515    "Option",
1516    "Ok",
1517    "Err",
1518    "Some",
1519    "None",
1520    "JsonError",
1521    "Uuid",
1522    "FetchError",
1523];
1524
1525/// The capability interfaces a vendored binding legitimately imports to implement what
1526/// it declares — governed by language-stability rules, not ADR 0310's codegen-freeze
1527/// concern. See [`emit_abi_shapes`] and #999 Decision E for the two-list rationale.
1528const CAPABILITY_SURFACE: &[&str] = &[
1529    "Clock",
1530    "Fetch",
1531    "Idempotency",
1532    "Locale",
1533    "Logger",
1534    "Random",
1535    "Secrets",
1536    "Request",
1537    "Response",
1538    "LocaleTag",
1539    "Kv",
1540    "KVNamespace",
1541];
1542
1543/// Is `ident` one of ADR 0310's enumerated emit-ABI shapes, or part of the capability
1544/// surface a binding is required to import? If neither, it's a leak `emit_abi_shapes`
1545/// flags — this is the single predicate both the probe and its tests use, so a test
1546/// asserting "no leak" can't silently pass against a list the test itself redefined.
1547fn is_enumerated_emit_abi_or_capability_surface(ident: &str) -> bool {
1548    EMIT_ABI.contains(&ident) || CAPABILITY_SURFACE.contains(&ident)
1549}
1550
1551/// ADR 0310's probe (#999 Decision E). The vendored first-party bindings under
1552/// `bynk-check/src/firstparty/bindings/` must reference only [`EMIT_ABI`]'s nine names.
1553///
1554/// This does NOT count every non-enumerated import: a binding legitimately imports the
1555/// [`CAPABILITY_SURFACE`] interfaces it implements — that surface is governed by
1556/// language-stability rules, not ADR 0310's codegen-freeze concern, and a probe that
1557/// flagged it would read non-zero on every binding by construction. See #999 Decision
1558/// E for the two-list rationale and its falsifier.
1559fn emit_abi_shapes(root: &Path) -> Probe {
1560    let dir = root.join("bynk-check/src/firstparty/bindings");
1561    let mut leaks: Vec<String> = Vec::new();
1562    let Ok(entries) = std::fs::read_dir(&dir) else {
1563        return Probe {
1564            name: "emit_abi_shapes",
1565            gated: true,
1566            reads: "bindings directory not found".to_string(),
1567        };
1568    };
1569    let mut files: Vec<_> = entries.flatten().map(|e| e.path()).collect();
1570    files.sort();
1571    for path in files {
1572        if path.extension().is_none_or(|e| e != "ts") {
1573            continue;
1574        }
1575        let Ok(contents) = std::fs::read_to_string(&path) else {
1576            continue;
1577        };
1578        let name = path.file_name().unwrap().to_string_lossy().to_string();
1579        for ident in ts_named_imports_from_runtime_modules(&contents) {
1580            if !is_enumerated_emit_abi_or_capability_surface(&ident) {
1581                leaks.push(format!("{name}:{ident}"));
1582            }
1583        }
1584    }
1585    Probe {
1586        name: "emit_abi_shapes",
1587        gated: true,
1588        reads: format!("{} ({})", leaks.len(), leaks.join(", ")),
1589    }
1590}
1591
1592// --- Gated probe 10: ts_writes ---------------------------------------------
1593
1594/// Files under `bynk-emit/src` that contain `write!`/`writeln!`/`format!` calls but
1595/// produce no TypeScript at all — excluded from both [`ts_writes`] and [`ts_any`], each
1596/// argued individually the same way [`AST_IMPORTER_EXCEPTIONS`] is, not assumed from a
1597/// path prefix: `emitter/wrangler.rs` writes `wrangler.toml`; `emitter/secrets.rs`
1598/// writes `bynk-secrets.json`; `emitter/contracts.rs` writes `bynk-contracts.json`;
1599/// `emitter/source_map.rs` writes source-map JSON; `testkit.rs` builds a `.bynk` source
1600/// fixture — a compiler *input* for tests, not output. P7.3 (#1303): `emitter/toml_doc.rs`
1601/// writes `wrangler.toml` text too — `emitter/wrangler.rs`'s own writes moved here when
1602/// it stopped building the TOML text directly and started building a typed
1603/// `TomlDocument` for this module to print — same rationale, same exclusion.
1604///
1605/// (`ir/lower.rs` — Rust-internal `String` values stored on `Ir*` struct fields during
1606/// the checker→IR lowering pass, never emitted syntax — was excluded here for the same
1607/// reason until Arc D's P7.12 crate carve moved it to `bynk-lower` entirely, outside
1608/// this probe's own `bynk-emit/src` universe; no exclusion needed for a file this probe
1609/// no longer walks.)
1610const TS_WRITES_EXCLUDED_FILES: &[&str] = &[
1611    "emitter/wrangler.rs",
1612    "emitter/toml_doc.rs",
1613    "emitter/secrets.rs",
1614    "emitter/contracts.rs",
1615    "emitter/source_map.rs",
1616    "testkit.rs",
1617];
1618
1619/// Is `rel_path` (relative to `bynk-emit/src`) one of [`TS_WRITES_EXCLUDED_FILES`]?
1620fn is_ts_writes_excluded_file(rel_path: &Path) -> bool {
1621    let rel = rel_path.to_string_lossy().replace('\\', "/");
1622    TS_WRITES_EXCLUDED_FILES.contains(&rel.as_str())
1623}
1624
1625/// True if `line` builds a filesystem path via `format!` rather than TypeScript text —
1626/// the `PathBuf::from(format!(...))`/`.join(format!(...))`/`.with_file_name(format!(...))`
1627/// idiom [`ts_writes`] excludes at line granularity, not by file, because the files it
1628/// appears in (`project.rs`, `project/tests_emit.rs`) are otherwise genuinely
1629/// TypeScript-producing.
1630///
1631/// **`.with_file_name(format!` found and added by Arc F's own item-4 investigation
1632/// (#1457):** `project.rs`'s `sibling_path` (`output_path.with_file_name(format!(
1633/// "{name}.{suffix}"))`) builds a sibling filesystem path the same way the two idioms
1634/// above do, but spelled with `.with_file_name(` — the prior substring match didn't
1635/// catch it, over-counting `ts_writes` by this one site.
1636fn is_path_construction_line(line: &str) -> bool {
1637    line.contains("PathBuf::from(format!")
1638        || line.contains(".join(format!")
1639        || line.contains(".with_file_name(format!")
1640}
1641
1642/// Relativises every path in [`rust_files`]'s output against `dir`, so [`ts_writes`]
1643/// and [`ts_any`]'s counting logic ([`ts_writes_violations`], [`ts_any_violations`])
1644/// takes the same `&[(PathBuf, String)]` shape [`production_std_fs_files`] does — an
1645/// in-memory file list a test can construct directly, per review of #1297 (a first cut
1646/// of these two probes took `root: &Path` and did its own walk, so nothing but the
1647/// drift gate actually exercised the exclusion logic; deleting a `continue` left every
1648/// test green).
1649fn rust_files_relative(dir: &Path) -> Vec<(PathBuf, String)> {
1650    rust_files(dir)
1651        .into_iter()
1652        .map(|(path, contents)| {
1653            let rel = path.strip_prefix(dir).unwrap_or(&path).to_path_buf();
1654            (rel, contents)
1655        })
1656        .collect()
1657}
1658
1659/// [`ts_writes`]'s counting logic, over an explicit `(relative path, contents)` list —
1660/// see [`rust_files_relative`] for why this isn't `root: &Path`.
1661///
1662/// **A real mistake this slice's own grounding found and fixed, not carried forward:**
1663/// an earlier survey (during phase 7's own track-opening research) characterised
1664/// `project/tests_emit.rs`'s 128 such sites as excludable "test-assertion strings" — the
1665/// same mischaracterisation `semantics-in-the-checker.md`'s own settling review caught
1666/// and corrected for a *different* probe (`emit_diagnostics`) on this same file: it is
1667/// `process_tests`/`process_integration_tests`, real production TypeScript-emission
1668/// code, not fixture noise, and none of its 128 sites fall inside its own single
1669/// `#[cfg(test)] mod tests { .. }` block. All 128 count here, less the one line that
1670/// genuinely builds a file path ([`is_path_construction_line`]).
1671///
1672/// **Known, accepted gap:** `project/tests_emit.rs`'s
1673/// `target_name: format!("integration · {suite}")` builds a human-readable struct-field
1674/// label, not TypeScript text, and matches neither exclusion rule. A text-level scanner
1675/// has no cheap way to catch one field-name-specific site without a bespoke rule for it
1676/// alone — accepted as a one-site over-count, the same "known remaining gaps, out of
1677/// reach for a text-level scanner" discipline [`production_std_fs_files`] already
1678/// documents for a different probe.
1679fn ts_writes_violations(files: &[(PathBuf, String)]) -> usize {
1680    let mut count = 0usize;
1681    for (rel, contents) in files {
1682        if is_ts_writes_excluded_file(rel) {
1683            continue;
1684        }
1685        let lines: Vec<&str> = contents.lines().collect();
1686        let ranges = test_mod_ranges(&lines);
1687        for (i, line) in lines.iter().enumerate() {
1688            if in_test_range(i, &ranges) || is_line_comment(line) || is_path_construction_line(line)
1689            {
1690                continue;
1691            }
1692            if line.contains("write!") || line.contains("writeln!") || line.contains("format!") {
1693                count += 1;
1694            }
1695        }
1696    }
1697    count
1698}
1699
1700/// The trajectory's own phase-7 probe (`design/bynk-compiler-trajectory.md` §3):
1701/// "TypeScript-producing `write!` outside a printer". Never measured before this slice
1702/// (P7.0, #1296; track doc `design/tracks/the-typescript-tree.md` §5, §6) — `bynk-ts`
1703/// does not exist yet, so "outside a printer" reduces today to "in `bynk-emit`, outside
1704/// a `Verbatim` construction"; the `Verbatim` half of that exclusion is vacuous until
1705/// phase 7's own P7.5 builds the type (track doc §5's own note).
1706///
1707/// **Not "zero/closure"-shaped like this module's other twelve gated probes, and gated
1708/// anyway — a deliberate choice, not an inherited one.** The reading is 1641, headed
1709/// toward a phase-7 floor named at that track's own retirement, not toward 0 or a small
1710/// fixed number the way `ast_importers`/`emit_abi_shapes` are. It moves on any
1711/// `bynk-emit` PR that adds or removes a single `write!`/`writeln!`/`format!` line
1712/// anywhere in the crate — the same volatility #999 Decision D cites for *not* gating
1713/// `wildcard_arms` (311, ungated for exactly this reason). Gated here anyway, because
1714/// this track's own Arc C is dozens of slices each claiming "I converted a file's
1715/// emission to the tree", and only a diffed, committed number makes that claim
1716/// CI-checkable rather than self-reported — the same trade `ast_importers` already made
1717/// successfully across phase 6's 59 slices, a probe with the identical shape (a large
1718/// count, converging over many slices, still gated throughout). The churn cost is real
1719/// and accepted, not overlooked: see `design/pending/p7-0-ts-writes-ts-any-probes.md`'s
1720/// own ADR for the argument in full (review of #1297).
1721///
1722/// Counts `bynk-emit/src/**/*.rs` lines — excluding comments, `#[cfg(test)]` test-module
1723/// ranges, [`TS_WRITES_EXCLUDED_FILES`], and [`is_path_construction_line`] matches —
1724/// containing `write!`, `writeln!` or `format!`. See [`ts_writes_violations`] for the
1725/// counting logic itself.
1726fn ts_writes(root: &Path) -> Probe {
1727    let dir = root.join("bynk-emit/src");
1728    Probe {
1729        name: "ts_writes",
1730        gated: true,
1731        reads: ts_writes_violations(&rust_files_relative(&dir)).to_string(),
1732    }
1733}
1734
1735// --- Gated probe 11: ts_any -------------------------------------------------
1736
1737/// True if `line` (not a comment) violates R7.1's `TsType::Any` prohibition: an
1738/// `as any` cast, a bare `: any` type annotation, or `any` in generic type-argument
1739/// position (`Array<any>`, `Record<string, any[]>`, `Promise<any>`).
1740///
1741/// Six patterns, not `as any` alone, following three rounds of the same finding.
1742/// Round one (Q3, `design/tracks/the-typescript-tree.md` §3.3) found `as any` alone
1743/// under-counts R7.1 and added bare `: any`. Round two (review of #1297) found *that*
1744/// still under-counts: `bynk-emit/src/emitter/lower.rs`'s `joinOn`/`leftJoin`/`groupBy`
1745/// emit `const __h: Record<string, any[]> = {}` — `, any[]` contains neither `as any`
1746/// nor `: any`, so three live, production, TypeScript-emitting sites read as clean
1747/// under the round-one predicate. Widened to also match `<any`, `any>` and `any[]` —
1748/// each checked against the live tree for false positives (no non-`any`-typed English
1749/// word starts with `any` immediately after `<` or ends in `any` immediately before
1750/// `>`/`[]` anywhere in `bynk-emit/src` today) rather than assumed safe. Round three
1751/// (review of #1322) found a fourth spelling: once a site builds a real `bynk_ts::
1752/// TsType` node instead of writing TypeScript text directly, an emitted `any` no
1753/// longer appears as Rust-source `as any`/`: any` at all — `workers.rs`'s own
1754/// `TsType::named("any")` calls (#1321) emit the identical `payload as any`/
1755/// `let __who: any` text as before, byte-for-byte, but the *Rust spelling* that
1756/// produces it no longer matches any of the five text patterns above, so the probe
1757/// silently uncounted three real, still-live R7.1 residuals. Every later Arc C slice
1758/// converting an `any`-emitting `writeln!`/`format!` site the same way would keep
1759/// deflating this count the same way, so the fix generalises rather than special-
1760/// cases these three lines: match the construction spelling itself
1761/// (`named("any"`), not just raw emitted text.
1762///
1763/// Split out from [`ts_any_violations`] so a test can exercise the predicate directly,
1764/// without file I/O.
1765fn line_violates_ts_any(line: &str) -> bool {
1766    !is_line_comment(line)
1767        && (line.contains("as any")
1768            || line.contains(": any")
1769            || line.contains("<any")
1770            || line.contains("any>")
1771            || line.contains("any[]")
1772            || line.contains("named(\"any\""))
1773}
1774
1775/// [`ts_any`]'s counting logic, over an explicit `(relative path, contents)` list — see
1776/// [`rust_files_relative`] for why this isn't `root: &Path`.
1777fn ts_any_violations(files: &[(PathBuf, String)]) -> usize {
1778    let mut count = 0usize;
1779    for (rel, contents) in files {
1780        if is_ts_writes_excluded_file(rel) {
1781            continue;
1782        }
1783        let lines: Vec<&str> = contents.lines().collect();
1784        let ranges = test_mod_ranges(&lines);
1785        for (i, line) in lines.iter().enumerate() {
1786            if in_test_range(i, &ranges) {
1787                continue;
1788            }
1789            if line_violates_ts_any(line) {
1790                count += 1;
1791            }
1792        }
1793    }
1794    count
1795}
1796
1797/// Reference rule R7.1 (`design/bynk-greenfield-compiler.md` Part 7) — "the tree
1798/// contains no ... `TsType::Any`". Gated for the same reason [`ts_writes`] is (see its
1799/// own doc comment): this reading, 55 (not the settling review's estimated ~24 — see
1800/// `design/pending/p7-0-ts-writes-ts-any-probes.md`), is this track's own second
1801/// completion ratchet, and only a diffed, committed number makes "I removed an `Any`"
1802/// CI-checkable per slice.
1803///
1804/// Counts `bynk-emit/src/**/*.rs` lines — excluding `#[cfg(test)]` test-module ranges
1805/// and [`TS_WRITES_EXCLUDED_FILES`] (the same files [`ts_writes`] excludes for producing
1806/// no TypeScript at all; an `any`-typed value there isn't R7.1's business either) —
1807/// matching [`line_violates_ts_any`]. Hand-written runtime `.ts` files under
1808/// `bynk-emit/runtime/` are out of scope by construction: [`rust_files`] only walks
1809/// `.rs` files, and R7.1 governs the emitted *tree*, not the hand-written runtime R7.7
1810/// separately covers.
1811fn ts_any(root: &Path) -> Probe {
1812    let dir = root.join("bynk-emit/src");
1813    Probe {
1814        name: "ts_any",
1815        gated: true,
1816        reads: ts_any_violations(&rust_files_relative(&dir)).to_string(),
1817    }
1818}
1819
1820// --- Gated probe 12: verbatim_origins ---------------------------------------
1821
1822/// P7.5 (#1307): distinct `bynk_ts::VerbatimOrigin` variants named in
1823/// `bynk-emit/src` — how many *families* of residual, not-yet-converted
1824/// emission remain, not their size (`verbatim_sites`, below, is the size).
1825/// Retires at an **argued floor**, expected small (1-3), named file-by-file
1826/// at retirement the way `ast_importers`'s floor of 5 was (`design/tracks/
1827/// the-typescript-tree.md` §5). Reads **0** at this slice's own landing:
1828/// `bynk-emit` builds no `Verbatim` content yet (#1307's Decision C) — Arc
1829/// C's own first slice is what gives this probe something to count.
1830///
1831/// Line-scans for `VerbatimOrigin::<Variant>` and counts distinct variant
1832/// names referenced, the same needle-scan shape [`hoist_sinks`] uses. A
1833/// known, accepted gap (review of #1308, finding 6): a bare `use
1834/// bynk_ts::VerbatimOrigin::Contracts;` followed by unqualified `Contracts`
1835/// elsewhere would undercount, since the needle is the qualified path. Not
1836/// worth a real-parser fix for an *argued-floor* probe (unlike
1837/// `verbatim_sites`'s own floor of exactly 0) — `bynk-emit`'s own existing
1838/// call-site style always qualifies (`TsStmt::verbatim(VerbatimOrigin::X,
1839/// …)`), so this is a theoretical undercount, not an observed one.
1840fn verbatim_origins(root: &Path) -> Probe {
1841    let dir = root.join("bynk-emit/src");
1842    Probe {
1843        name: "verbatim_origins",
1844        gated: true,
1845        reads: verbatim_origins_violations(&rust_files_relative(&dir)).to_string(),
1846    }
1847}
1848
1849/// [`verbatim_origins`]'s counting logic, over an explicit `(relative path,
1850/// contents)` list — see [`rust_files_relative`] for why this isn't `root:
1851/// &Path`. Excludes `#[cfg(test)]` ranges the same way [`ts_any_violations`]
1852/// does (review of #1308, finding 6): without this, one `bynk-emit` unit
1853/// test constructing a `VerbatimOrigin` for its own fixture pins this probe
1854/// above its argued floor permanently, for a reason that has nothing to do
1855/// with residual production emission.
1856fn verbatim_origins_violations(files: &[(PathBuf, String)]) -> usize {
1857    let needle = "VerbatimOrigin::";
1858    let mut variants: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1859    for (_, contents) in files {
1860        let lines: Vec<&str> = contents.lines().collect();
1861        let ranges = test_mod_ranges(&lines);
1862        for (i, line) in lines.iter().enumerate() {
1863            if in_test_range(i, &ranges) || is_line_comment(line) {
1864                continue;
1865            }
1866            let mut rest = *line;
1867            while let Some(idx) = rest.find(needle) {
1868                let after = &rest[idx + needle.len()..];
1869                let name: String = after
1870                    .chars()
1871                    .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
1872                    .collect();
1873                rest = &after[name.len()..];
1874                if !name.is_empty() {
1875                    variants.insert(name);
1876                }
1877            }
1878        }
1879    }
1880    variants.len()
1881}
1882
1883// --- Gated probe 13: verbatim_sites -----------------------------------------
1884
1885/// P7.5 (#1307): distinct `TsStmt::verbatim(...)` construction call sites in
1886/// `bynk-emit/src`, line-scanned the same way [`hoist_sinks`] counts
1887/// `stmts: &mut Vec<String>` occurrences. Retires at **0**: every call site
1888/// converting to a real tree node is what Arc C's own per-file slices are
1889/// actually for (`design/tracks/the-typescript-tree.md` §5) —
1890/// `verbatim_origins` alone can't distinguish "3 variants, 12 residual call
1891/// sites" from "3 variants, 900 residual call sites, two files never
1892/// decomposed"; this is what closes that gap. Reads **0** at this slice's
1893/// own landing, same reason `verbatim_origins` does.
1894fn verbatim_sites(root: &Path) -> Probe {
1895    let dir = root.join("bynk-emit/src");
1896    Probe {
1897        name: "verbatim_sites",
1898        gated: true,
1899        reads: verbatim_sites_violations(&rust_files_relative(&dir)).to_string(),
1900    }
1901}
1902
1903/// [`verbatim_sites`]'s counting logic, over an explicit `(relative path,
1904/// contents)` list — see [`rust_files_relative`] for why this isn't `root:
1905/// &Path`. Excludes `#[cfg(test)]` ranges the same way [`ts_any_violations`]
1906/// does (review of #1308, finding 6): `verbatim_sites` is documented as
1907/// retiring at 0, so a residual construction site inside a test fixture
1908/// would pin it above zero permanently for a reason that has nothing to do
1909/// with production emission conversion.
1910fn verbatim_sites_violations(files: &[(PathBuf, String)]) -> usize {
1911    let needle = "TsStmt::verbatim(";
1912    let mut count = 0usize;
1913    for (_, contents) in files {
1914        let lines: Vec<&str> = contents.lines().collect();
1915        let ranges = test_mod_ranges(&lines);
1916        for (i, line) in lines.iter().enumerate() {
1917            if in_test_range(i, &ranges) {
1918                continue;
1919            }
1920            if !is_line_comment(line) && line.contains(needle) {
1921                count += 1;
1922            }
1923        }
1924    }
1925    count
1926}
1927
1928/// Named identifiers imported from the compiler-generated firstparty/runtime relative
1929/// modules (`./bynk.js`, `./runtime.js`, `./bynk/locale/types.js`, `./cloudflare.js`,
1930/// or their `../` forms) — `import type { A, B }`/`import { A, B }` braces, stripping
1931/// `type ` markers and `X as Y` aliases (keeping the imported name, not the local one,
1932/// since the allowlists are about what's referenced, not what it's called locally).
1933fn ts_named_imports_from_runtime_modules(src: &str) -> Vec<String> {
1934    let mut out = Vec::new();
1935    for line in src.lines() {
1936        let line = line.trim();
1937        if !line.starts_with("import") {
1938            continue;
1939        }
1940        let is_runtime_module = ["\"./bynk.js\"", "\"./runtime.js\"", "\"../runtime.js\""]
1941            .iter()
1942            .any(|m| line.ends_with(&format!("from {m};")))
1943            || line.contains("bynk/locale/types.js")
1944            || line.contains("cloudflare.js");
1945        if !is_runtime_module {
1946            continue;
1947        }
1948        let Some(open) = line.find('{') else { continue };
1949        let Some(close) = line.find('}') else {
1950            continue;
1951        };
1952        for part in line[open + 1..close].split(',') {
1953            let part = part.trim().trim_start_matches("type ").trim();
1954            if part.is_empty() {
1955                continue;
1956            }
1957            let imported = part.split(" as ").next().unwrap_or(part).trim();
1958            out.push(imported.to_string());
1959        }
1960    }
1961    out
1962}
1963
1964// --- Reported probe 1: wildcard_arms --------------------------------------
1965
1966/// R2.12. `clippy::wildcard_enum_match_arm` diagnostics, forced on via `-W` so the
1967/// count is real from day one and doesn't wait on `workspace_lints`/T0.3 (#999 Decision
1968/// C — delegating to clippy's own type-aware pass, rather than a hand-rolled scan for
1969/// "compiler-owned enum", so the probe and the enforcement mechanism can never
1970/// disagree). A count, not a boolean — moves on nearly every match statement anyone
1971/// writes, so it is reported, not gated (#999 Decision D).
1972fn wildcard_arms(root: &Path) -> Probe {
1973    let reads = match run_clippy_wildcard_scan(root) {
1974        Ok(n) => n.to_string(),
1975        Err(e) => format!("error running clippy: {e}"),
1976    };
1977    Probe {
1978        name: "wildcard_arms",
1979        gated: false,
1980        reads,
1981    }
1982}
1983
1984/// Runs clippy with the lint forced on and parses the NDJSON output properly —
1985/// **not** a substring count. A single `wildcard_enum_match_arm` diagnostic's JSON
1986/// repeats the lint name several times (the `code` field, the human-readable message,
1987/// the `#[warn(...)]` note, and the `rendered` field duplicating the whole thing as
1988/// text), so `stdout.matches("wildcard_enum_match_arm").count()` overcounts by roughly
1989/// 3x — caught by cross-checking this probe's own first run against a real JSON parse
1990/// (296 real diagnostics, not the naive scan's 888).
1991///
1992/// Checks the process exit status: a forced `-W` (not `-D`) never fails the build on
1993/// account of the lint itself, so a non-zero exit means clippy genuinely could not run
1994/// (a compile error elsewhere, a missing toolchain component, offline with no cached
1995/// index) — in which case stdout carries no `compiler-message` lines and a silent
1996/// success would report a false, and indistinguishable, `0`. This probe is reported,
1997/// not gated, precisely so an honest "couldn't measure" surfaces loudly here rather
1998/// than being read as "closed."
1999fn run_clippy_wildcard_scan(root: &Path) -> std::io::Result<usize> {
2000    let output = Command::new("cargo")
2001        .args([
2002            "clippy",
2003            "--workspace",
2004            "--message-format=json",
2005            "--",
2006            "-W",
2007            "clippy::wildcard_enum_match_arm",
2008        ])
2009        .current_dir(root)
2010        .output()?;
2011    if !output.status.success() {
2012        return Err(std::io::Error::other(format!(
2013            "cargo clippy exited with {}: {}",
2014            output.status,
2015            String::from_utf8_lossy(&output.stderr).trim()
2016        )));
2017    }
2018    let stdout = String::from_utf8_lossy(&output.stdout);
2019    let mut count = 0usize;
2020    for line in stdout.lines() {
2021        let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
2022            continue;
2023        };
2024        if value.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") {
2025            continue;
2026        }
2027        let code = value.pointer("/message/code/code").and_then(|c| c.as_str());
2028        if code == Some("clippy::wildcard_enum_match_arm") {
2029            count += 1;
2030        }
2031    }
2032    Ok(count)
2033}
2034
2035// --- Reported probe 2: keep_in_sync ---------------------------------------
2036
2037/// P2 (trend only). Comments across the workspace containing "in sync", "mirrors",
2038/// "parity", or "must match" — each one names a rule the compiler cannot teach itself
2039/// and must be taught in review, every time.
2040fn keep_in_sync(root: &Path) -> Probe {
2041    let phrases = ["in sync", "mirrors", "parity", "must match"];
2042    let mut count = 0usize;
2043    for dir in top_level_crate_dirs(root) {
2044        for (_, contents) in rust_files(&dir.join("src")) {
2045            for line in contents.lines() {
2046                if is_line_comment(line) {
2047                    let lower = line.to_lowercase();
2048                    if phrases.iter().any(|p| lower.contains(p)) {
2049                        count += 1;
2050                    }
2051                }
2052            }
2053        }
2054    }
2055    Probe {
2056        name: "keep_in_sync",
2057        gated: false,
2058        reads: count.to_string(),
2059    }
2060}
2061
2062// --- Reported probe 3: test_density ---------------------------------------
2063
2064/// R11.1, and §3.4's phase-3 trigger. Per crate: (lines inside `#[test]` fn bodies,
2065/// plus lines inside `#[cfg(test)] mod` blocks outside those fns) ÷ (non-blank,
2066/// non-comment lines under that crate's `src/`) — #999 Decision F's definition,
2067/// written down precisely because an undefined "ratio" is exactly the ambiguity that
2068/// produced the track doc §9's four-row ambiguity.
2069fn test_density(root: &Path) -> Probe {
2070    let mut parts = Vec::new();
2071    for dir in top_level_crate_dirs(root) {
2072        let name = dir.file_name().unwrap().to_string_lossy().to_string();
2073        let src_dir = dir.join("src");
2074        let mut test_lines = 0usize;
2075        let mut code_lines = 0usize;
2076        for (_, contents) in rust_files(&src_dir) {
2077            let lines: Vec<&str> = contents.lines().collect();
2078            let ranges = test_mod_ranges(&lines);
2079            for (i, line) in lines.iter().enumerate() {
2080                let is_blank_or_comment = line.trim().is_empty() || is_line_comment(line);
2081                if !is_blank_or_comment {
2082                    code_lines += 1;
2083                }
2084                if in_test_range(i, &ranges) && !is_blank_or_comment {
2085                    test_lines += 1;
2086                }
2087            }
2088        }
2089        if code_lines > 0 {
2090            let ratio = 100.0 * test_lines as f64 / code_lines as f64;
2091            parts.push(format!("{name}={ratio:.1}%"));
2092        }
2093    }
2094    Probe {
2095        name: "test_density",
2096        gated: false,
2097        reads: parts.join(", "),
2098    }
2099}
2100
2101// --- Reported probe 4: fixture_kinds --------------------------------------
2102
2103/// R11.2. Fixture directories under `bynkc/tests` using each assertion granularity —
2104/// `expected_contains.txt` / `expected_absent.txt` / `expected_diagnostics.txt` — set
2105/// against the older, coarser `expected_error.txt` (category-string) convention.
2106fn fixture_kinds(root: &Path) -> Probe {
2107    let tests_dir = root.join("bynkc/tests");
2108    let contains = count_files_named(&tests_dir, "expected_contains.txt");
2109    let absent = count_files_named(&tests_dir, "expected_absent.txt");
2110    let diagnostics = count_files_named(&tests_dir, "expected_diagnostics.txt");
2111    let error = count_files_named(&tests_dir, "expected_error.txt");
2112    Probe {
2113        name: "fixture_kinds",
2114        gated: false,
2115        reads: format!(
2116            "contains={contains}, absent={absent}, diagnostics={diagnostics}, error={error}"
2117        ),
2118    }
2119}
2120
2121fn count_files_named(dir: &Path, filename: &str) -> usize {
2122    let mut count = 0usize;
2123    count_files_named_walk(dir, filename, &mut count);
2124    count
2125}
2126
2127fn count_files_named_walk(dir: &Path, filename: &str, count: &mut usize) {
2128    let Ok(entries) = std::fs::read_dir(dir) else {
2129        return;
2130    };
2131    for entry in entries.flatten() {
2132        let path = entry.path();
2133        if path.is_dir() {
2134            count_files_named_walk(&path, filename, count);
2135        } else if path.file_name().is_some_and(|n| n == filename) {
2136            *count += 1;
2137        }
2138    }
2139}
2140
2141// --- Rendering + diffing ---------------------------------------------------
2142
2143/// The committed table: a plain Markdown table, probe name → gated?/reads, plus a
2144/// pointer to the rule ledger `stamp::apply` writes (#1001).
2145pub fn render_table(report: &Report) -> String {
2146    let mut out = String::new();
2147    out.push_str("<!-- GENERATED FILE — do not edit by hand.\n");
2148    out.push_str("     Source: cargo xtask greenfield-status (xtask/src/greenfield_status.rs).\n");
2149    out.push_str("     Regenerate with: cargo xtask greenfield-status --apply -->\n\n");
2150    out.push_str("# Greenfield status\n\n");
2151    out.push_str(
2152        "Track slice T0.0 (#999); `ts_writes`/`ts_any` added by P7.0 (#1296); \
2153         `verbatim_origins`/`verbatim_sites` added by P7.5 (#1307). Thirteen \
2154         probes are gated — a disagreement between this file and a fresh run fails \
2155         `greenfield_status_table_is_current` (`xtask/tests/greenfield_status.rs`). \
2156         Four are trend probes, reported only.\n\n",
2157    );
2158    out.push_str("| Probe | Gated | Reads |\n|---|---|---|\n");
2159    for probe in &report.probes {
2160        let _ = writeln!(
2161            out,
2162            "| `{}` | {} | {} |",
2163            probe.name,
2164            if probe.gated { "yes" } else { "no (trend)" },
2165            probe.reads
2166        );
2167    }
2168
2169    out.push_str("\n## Rules closed\n\n");
2170    // A static, unconditional link — not a count, and not even an existence
2171    // check. A first draft read `design/greenfield-status-rules.md` here to
2172    // report a row count, but nothing regenerates *this* file when `stamp`
2173    // writes the ledger (`stamp.yml` never runs `greenfield-status --apply`,
2174    // and the gating test only diffs the nine probes) — so a count or an
2175    // exists/doesn't-exist message would silently go stale the moment the
2176    // first `closes_rule` landed, which is exactly the drift this section
2177    // exists to avoid, not invite (#1001 review). Static text can't go stale;
2178    // the ledger is one click away either way.
2179    out.push_str(
2180        "See [`design/greenfield-status-rules.md`](greenfield-status-rules.md) for rule ids \
2181         closed so far (written by `cargo xtask stamp --apply` at merge; may not exist yet if \
2182         no increment has cited `closes_rule`).\n",
2183    );
2184    out
2185}
2186
2187/// Every gated probe whose live reading disagrees with the committed table's, as
2188/// `(probe name, committed, live)`. Trend probes are never compared, and never
2189/// computed here — this only runs the thirteen gated probes, so checking currency never
2190/// pays for `wildcard_arms`'s workspace-wide clippy pass. For a caller that has already
2191/// run the full report (e.g. to print it), use [`gated_disagreements_in`] instead so the
2192/// thirteen gated probes aren't computed a second time.
2193pub fn gated_disagreements(root: &Path) -> Vec<(String, String, String)> {
2194    gated_disagreements_in(&run_gated(root), root)
2195}
2196
2197/// Like [`gated_disagreements`], but diffs `probes` (typically a [`Report`]'s
2198/// `.probes`, already computed) instead of re-running the gated probes.
2199pub fn gated_disagreements_in(probes: &[Probe], root: &Path) -> Vec<(String, String, String)> {
2200    let committed = std::fs::read_to_string(table_path(root)).unwrap_or_default();
2201    let mut out = Vec::new();
2202    for probe in probes.iter().filter(|p| p.gated) {
2203        let row_prefix = format!("| `{}` | yes | ", probe.name);
2204        let committed_reads = committed
2205            .lines()
2206            .find(|l| l.starts_with(&row_prefix))
2207            .and_then(|l| l.strip_prefix(&row_prefix))
2208            .and_then(|l| l.strip_suffix(" |"))
2209            .unwrap_or("<row missing>");
2210        if committed_reads != probe.reads {
2211            out.push((
2212                probe.name.to_string(),
2213                committed_reads.to_string(),
2214                probe.reads.clone(),
2215            ));
2216        }
2217    }
2218    out
2219}
2220
2221#[cfg(test)]
2222mod tests {
2223    use super::*;
2224
2225    // --- emit_diagnostics (#999 Decision A) ---------------------------------
2226
2227    /// A standalone `"bynk.foo"` literal is found — the ordinary case.
2228    #[test]
2229    fn bynk_dotted_literals_finds_standalone_literal() {
2230        let src = r#"code("bynk.check.something", "a message")"#;
2231        assert_eq!(bynk_dotted_literals(src), vec!["bynk.check.something"]);
2232    }
2233
2234    /// The bug this slice found in its own first draft: a longer message that merely
2235    /// *starts* with "bynk." must not be truncated into a fake code literal. Regression
2236    /// test for `bynk.map itself uses bynk.list, so list must be injected too: {paths:?}`
2237    /// (`bynk-emit/src/project.rs`), which an earlier, less careful version of this scan
2238    /// wrongly counted as the literal `"bynk.map"`.
2239    #[test]
2240    fn bynk_dotted_literals_ignores_prefix_of_a_longer_message() {
2241        let src = r#"assert!(cond, "bynk.map itself uses bynk.list, so list must be injected too: {paths:?}");"#;
2242        assert!(bynk_dotted_literals(src).is_empty());
2243    }
2244
2245    /// Regression test for the other half of the same bug: a `\`-continued string
2246    /// literal (`"bynk.emit.unresolved_cross_context_signature: no signature for \`,
2247    /// continued on the next source line) is one string, not a diagnostic-code literal,
2248    /// even though its first segment matches the identifier charset — because the
2249    /// character after the run is `:`, never a closing quote, on either line.
2250    #[test]
2251    fn bynk_dotted_literals_ignores_a_line_continued_message() {
2252        let src =
2253            "\"bynk.emit.unresolved_cross_context_signature: no signature for \\\n     the rest\"";
2254        assert!(bynk_dotted_literals(src).is_empty());
2255    }
2256
2257    /// The whole point of Decision A: cross-referencing the real registry, not a
2258    /// hand-maintained exclusion list, correctly separates a real diagnostic code from
2259    /// a commons/namespace path that merely looks like one.
2260    #[test]
2261    fn emit_diagnostics_cross_references_the_real_registry() {
2262        let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
2263            .iter()
2264            .map(|d| d.code)
2265            .collect();
2266        // A code this registry is known to carry (bynk-syntax/src/diagnostics.rs).
2267        assert!(registry.contains("bynk.parse.expected_expression"));
2268        // A commons/namespace path, not a diagnostic code — #999's own verified survey.
2269        assert!(!registry.contains("bynk.locale"));
2270    }
2271
2272    // --- ast_importers (#1176) ------------------------------------------------
2273
2274    /// The exclusion is named, not prefixed: `project/tests_emit.rs` is the
2275    /// Q7-settled `Ir → String` half that keeps hand-writing TypeScript by calling
2276    /// straight into `emitter.rs`'s own body-rendering, and
2277    /// keeps reading a handler's declared param/return `TypeRef` with no `TyId`
2278    /// available at that call site — but `project.rs` (which also imports
2279    /// `bynk_syntax::ast`, via `EmitProjectCtx`) must stay counted, and so, per
2280    /// review of #1210, must `emitter.rs`/`emitter/lower.rs` themselves: both still
2281    /// hold live AST-*declaration* reads (`emitter.rs`'s `CommonsItem::Service`/
2282    /// `svc.protocol` walk, `emitter/lower.rs`'s `cap_op_param_names`) that are the
2283    /// still-open R6.13 defect this probe tracks, not the Q7 kind — excluding either
2284    /// file would hide that real work the same way a path-prefix rule would. A
2285    /// path-prefix rule (e.g. "only `emitter/**` counts") would have excluded
2286    /// `project.rs` right along with the legitimate ones, silently undercounting
2287    /// real work. (`ir.rs`/`ir/lower.rs`, the lowering pass's own former `Ast → Ir`
2288    /// exclusion, left this list at Arc D's P7.12 crate carve — they left
2289    /// `bynk-emit/src` entirely, not merely this list.)
2290    #[test]
2291    fn ast_importer_exclusion_is_named_not_prefixed() {
2292        assert!(is_named_ast_importer(Path::new("project/tests_emit.rs")));
2293        assert!(is_named_ast_importer(Path::new("emitter/serialisation.rs")));
2294        assert!(!is_named_ast_importer(Path::new("project.rs")));
2295        assert!(!is_named_ast_importer(Path::new("emitter.rs")));
2296        assert!(!is_named_ast_importer(Path::new("emitter/lower.rs")));
2297        assert!(!is_named_ast_importer(Path::new("emitter/workers.rs")));
2298        assert!(!is_named_ast_importer(Path::new("ir.rs")));
2299        assert!(!is_named_ast_importer(Path::new("ir/lower.rs")));
2300    }
2301
2302    /// #1184 review: an `AST_IMPORTER_EXCEPTIONS` entry going stale (renamed or split,
2303    /// e.g. `ir/lower.rs` becoming `ir/lower/mod.rs`) must fail loud here, not surface
2304    /// as a silent `ast_importers` regression in `greenfield_status_table_is_current` —
2305    /// mirrors [`file_is_named_fs_floor`]'s own "fail loud, not quiet" discipline.
2306    #[test]
2307    fn ast_importer_exceptions_still_exist_and_still_import_the_ast() {
2308        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2309            .join("..")
2310            .join("bynk-emit/src");
2311        for rel in AST_IMPORTER_EXCEPTIONS {
2312            let contents = std::fs::read_to_string(dir.join(rel)).unwrap_or_else(|e| {
2313                panic!("AST_IMPORTER_EXCEPTIONS entry {rel:?} does not exist: {e}")
2314            });
2315            assert!(
2316                contents.contains("bynk_syntax::ast"),
2317                "AST_IMPORTER_EXCEPTIONS entry {rel:?} no longer imports bynk_syntax::ast \
2318                 — it excludes nothing and should be removed"
2319            );
2320        }
2321    }
2322
2323    /// #1184 review, extended by #1187's own closing scoping pass (and narrowed by
2324    /// review of #1210, which found `emitter.rs`/`emitter/lower.rs` still hold live,
2325    /// in-scope AST-declaration reads and must stay counted) and by P6.33's own
2326    /// re-settling (`emitter/serialisation.rs`, a phase boundary rather than a
2327    /// declaration-read exemption): exercises the real filter over the live tree, not
2328    /// just the pure predicate — the survivor set the PR's own named-vs-prefix
2329    /// argument depends on: the named exclusions drop out
2330    /// (`project/tests_emit.rs`'s Q7-settled `Ir → String` case and
2331    /// `emitter/serialisation.rs`'s phase-7 codec renderer), while `emitter.rs`/
2332    /// `emitter/lower.rs`/`emitter/workers.rs` do not. `ir.rs`/`ir/lower.rs` (the
2333    /// lowering pass's own former `Ast → Ir` pair, excluded here until Arc D's
2334    /// P7.12 crate carve) are asserted absent below for a different reason now:
2335    /// they left `bynk-emit/src` entirely, so `ast_importer_files` never walks
2336    /// them at all, named exclusion or not.
2337    ///
2338    /// P6.49 (phase 6's own §6b): `project.rs` and `project/diagnostics.rs`
2339    /// join the *excluded* side of this assertion — the opposite of what this test
2340    /// checked before. `project.rs` cleared without joining
2341    /// [`AST_IMPORTER_EXCEPTIONS`]: nine slices (P6.42–P6.49) either relocated its
2342    /// remaining declaration reads to the `bynk-check`/`bynk-project` crates that
2343    /// already own the data, or re-exported a type from a `bynk-check` module whose
2344    /// own public API was already parameterised by it (the P6.27 `ExprId` precedent,
2345    /// applied to `TypeDecl`/`FnDecl`/`Visibility`/`ActorDecl`) — real, verified
2346    /// movement, not a probe exemption. `project/diagnostics.rs` rides on it, per the
2347    /// same super-glob rule this file's own regression guard below pins.
2348    #[test]
2349    fn ast_importers_excludes_the_named_pairs_and_project_rs() {
2350        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
2351        let dir = root.join("bynk-emit/src");
2352        let counted: BTreeSet<String> = ast_importer_files(&root)
2353            .into_iter()
2354            .map(|path| {
2355                path.strip_prefix(&dir)
2356                    .unwrap_or(&path)
2357                    .to_string_lossy()
2358                    .replace('\\', "/")
2359            })
2360            .collect();
2361        assert!(!counted.contains("ir.rs"), "moved to bynk-ir at P7.12");
2362        assert!(
2363            !counted.contains("ir/lower.rs"),
2364            "moved to bynk-lower at P7.12"
2365        );
2366        assert!(!counted.contains("project/tests_emit.rs"));
2367        assert!(!counted.contains("emitter/serialisation.rs"));
2368        assert!(!counted.contains("project.rs"));
2369        assert!(!counted.contains("project/diagnostics.rs"));
2370        assert!(counted.contains("emitter.rs"));
2371        assert!(counted.contains("emitter/lower.rs"));
2372        assert!(counted.contains("emitter/workers.rs"));
2373    }
2374
2375    /// P6.26 review (#1259): a module-level `use super::*;` is a real inheritance
2376    /// channel (Rust's own privacy rule makes a parent's private `use` visible to
2377    /// descendants) — must be detected — but a `use super::*;` nested inside a
2378    /// `#[cfg(test)] mod tests { .. }` block glob-imports its own *immediately
2379    /// enclosing* module, not the grandparent file on disk, and must not
2380    /// false-positive.
2381    #[test]
2382    fn module_level_super_glob_detection_ignores_nested_test_mod() {
2383        assert!(has_module_level_super_glob(
2384            "use std::fmt;\nuse super::*;\n"
2385        ));
2386        assert!(!has_module_level_super_glob(
2387            "fn f() {}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n}\n"
2388        ));
2389    }
2390
2391    /// P6.26 review (#1259): pins the real scenario the review found —
2392    /// `emitter/emit.rs` and `emitter/lower.rs` both carry a live, module-level
2393    /// `use super::*;` inheriting from `emitter.rs`, which itself still imports
2394    /// `bynk_syntax::ast` directly. Regression guard: if a future slice deletes
2395    /// either child's own explicit AST import while this inheritance channel and
2396    /// the parent's own AST dependency both remain, [`ast_importer_files`] must
2397    /// keep counting it rather than silently dropping the probe.
2398    #[test]
2399    fn super_glob_children_of_an_ast_importing_parent_are_detected() {
2400        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
2401        let dir = root.join("bynk-emit/src");
2402        for rel in ["emitter/emit.rs", "emitter/lower.rs"] {
2403            let contents = std::fs::read_to_string(dir.join(rel))
2404                .unwrap_or_else(|e| panic!("{rel:?} does not exist: {e}"));
2405            assert!(
2406                has_module_level_super_glob(&contents),
2407                "{rel:?} no longer carries a module-level `use super::*;` — this \
2408                 regression guard (and the false-zero hazard it pins) no longer applies \
2409                 and may be deleted"
2410            );
2411            assert_eq!(
2412                super_glob_parent_imports_ast(&dir, Path::new(rel)),
2413                Some(true),
2414                "{rel:?}'s parent (`emitter.rs`) no longer imports bynk_syntax::ast — \
2415                 update this guard's expectation"
2416            );
2417        }
2418        // A file directly under `bynk-emit/src` (no directory component) has no
2419        // `use super::*;` parent to inherit from.
2420        assert_eq!(
2421            super_glob_parent_imports_ast(&dir, Path::new("emitter.rs")),
2422            None
2423        );
2424    }
2425
2426    // --- fs_below_driver / test_density (trailing `#[cfg(test)] mod tests {}`) ---
2427
2428    #[test]
2429    fn production_std_fs_usage_is_detected() {
2430        let src = "fn load(p: &Path) -> String {\n    std::fs::read_to_string(p).unwrap()\n}\n";
2431        assert!(has_production_std_fs(src));
2432    }
2433
2434    #[test]
2435    fn std_fs_inside_a_trailing_test_mod_is_not_production() {
2436        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";
2437        assert!(!has_production_std_fs(src));
2438    }
2439
2440    /// Regression test for the other real bug this slice found: `bynk-emit/src/lib.rs`
2441    /// has `#[cfg(test)] pub(crate) mod testkit;` — an external-file module
2442    /// *declaration* (ends in `;`), not an inline block. It must not be mistaken for a
2443    /// scope-opening `mod tests { ... }`, or the (genuinely production) code after it in
2444    /// the same file would be wrongly excluded.
2445    #[test]
2446    fn cfg_test_external_mod_declaration_does_not_open_a_test_region() {
2447        let src = "#[cfg(test)]\npub(crate) mod testkit;\n\nfn load(p: &Path) -> String {\n    std::fs::read_to_string(p).unwrap()\n}\n";
2448        assert!(has_production_std_fs(src));
2449    }
2450
2451    /// Regression test for the bug caught in review: a file with **two** scattered
2452    /// `#[cfg(test)] mod ... { ... }` blocks, with real production code between them —
2453    /// exactly `bynk-emit/src/emitter/lower.rs`'s shape (two test modules, 1031
2454    /// production lines apart). A single "everything from the first/last `#[cfg(test)]`
2455    /// onward" cutoff would misclassify `lower_lambda` here as test-scope; the fix must
2456    /// close each block at its own boundary and resume production scanning after it.
2457    #[test]
2458    fn production_code_between_two_scattered_test_mods_is_detected() {
2459        let src = "\
2460#[cfg(test)]
2461mod decode_map_key_tests {
2462    #[test]
2463    fn t() {
2464        assert_eq!(1, 1);
2465    }
2466}
2467
2468fn lower_lambda(p: &Path) -> String {
2469    std::fs::read_to_string(p).unwrap()
2470}
2471
2472#[cfg(test)]
2473mod idempotency_scoping_tests {
2474    #[test]
2475    fn t2() {
2476        assert_eq!(2, 2);
2477    }
2478}
2479";
2480        assert!(has_production_std_fs(src));
2481    }
2482
2483    /// The same fixture's `test_mod_ranges` shape, checked directly: two disjoint
2484    /// ranges, not one span from the first block to the last.
2485    #[test]
2486    fn test_mod_ranges_finds_each_block_separately() {
2487        let src = "\
2488#[cfg(test)]
2489mod a {
2490    fn x() {}
2491}
2492
2493fn production() {}
2494
2495#[cfg(test)]
2496mod b {
2497    fn y() {}
2498}
2499";
2500        let lines: Vec<&str> = src.lines().collect();
2501        let ranges = test_mod_ranges(&lines);
2502        assert_eq!(
2503            ranges.len(),
2504            2,
2505            "expected two disjoint test-mod ranges: {ranges:?}"
2506        );
2507        // Line 5 (0-indexed) is `fn production() {}`, between the two blocks.
2508        assert!(
2509            !in_test_range(5, &ranges),
2510            "production() must not read as test-scope"
2511        );
2512    }
2513
2514    /// Regression test for the bug in the *fix* for the above: a column-0-`}`
2515    /// shortcut (tried and reverted during review) truncates a test module the moment
2516    /// its body embeds a multi-line fixture string containing a `}` flush against the
2517    /// left margin — exactly `bynk-ide/src/sequence.rs`'s shape, whose test mod embeds
2518    /// `.bynk` source fixtures. The real brace-depth scanner must see through the
2519    /// string and find the module's *actual* closing brace, hundreds of lines later.
2520    /// Uses a raw string for the outer fixture so the embedded `"..."` doesn't need
2521    /// escaping, and locates the real end by content rather than a hand-counted index
2522    /// — a hand-counted line number is exactly the kind of easy-to-miscount detail
2523    /// this codebase's own convention (verify, don't assume) warns against.
2524    #[test]
2525    fn test_mod_ranges_is_not_fooled_by_a_column_zero_brace_inside_a_string() {
2526        let src = r#"#[cfg(test)]
2527mod tests {
2528    const FIXTURE: &str = "
2529commons app.demo {
2530}
2531";
2532
2533    fn real_end_of_module() {}
2534}
2535"#;
2536        let lines: Vec<&str> = src.lines().collect();
2537        let ranges = test_mod_ranges(&lines);
2538        assert_eq!(ranges.len(), 1, "expected exactly one range: {ranges:?}");
2539        let (_, end) = ranges[0];
2540        // `str::lines()` drops the trailing newline, so the module's real closing
2541        // brace — the fixture's last line — is at `lines.len() - 1`. The string's
2542        // embedded `}` (an earlier line) must not be mistaken for it.
2543        assert_eq!(
2544            end,
2545            lines.len() - 1,
2546            "closed too early — mistook the string's `}}` for the module's: {ranges:?}"
2547        );
2548    }
2549
2550    // --- fs_below_driver: import resolution through `use super::*;` (#1013) ---
2551
2552    /// Run [`production_std_fs_files`] over an in-memory crate layout and name the
2553    /// flagged files, so each case reads as "these files, and only these".
2554    fn flagged(files: &[(&str, &str)]) -> Vec<String> {
2555        let owned: Vec<(PathBuf, String)> = files
2556            .iter()
2557            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
2558            .collect();
2559        production_std_fs_files(&owned)
2560            .into_iter()
2561            .map(|i| files[i].0.to_string())
2562            .collect()
2563    }
2564
2565    /// The concrete #1013 instance, in miniature: `project.rs` has a module-level
2566    /// `use std::fs;` (ancestor-scoped, so visible to descendants), `discovery.rs`
2567    /// glob-imports it via `use super::*;` and calls bare `fs::read_to_string` —
2568    /// touching `std::fs` in production while never spelling it. The text scan alone
2569    /// reads only `project.rs`; the resolved probe must read both.
2570    #[test]
2571    fn bare_fs_reached_through_a_glob_imported_parent_is_flagged() {
2572        let files = [
2573            ("lib.rs", "mod project;\n"),
2574            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2575            (
2576                "project/discovery.rs",
2577                "use super::*;\n\nfn read_source(path: &std::path::Path) -> String {\n    fs::read_to_string(path).unwrap()\n}\n",
2578            ),
2579        ];
2580        assert!(
2581            !has_production_std_fs(files[2].1),
2582            "the text scan alone must miss it"
2583        );
2584        assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
2585    }
2586
2587    /// Without `use super::*;` there is no path from the bare `fs::` to the parent's
2588    /// binding — the probe must not guess one into existence.
2589    #[test]
2590    fn bare_fs_without_a_glob_super_import_is_not_flagged() {
2591        let files = [
2592            ("lib.rs", "mod project;\n"),
2593            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2594            (
2595                "project/discovery.rs",
2596                "fn read_source(path: &std::path::Path) -> String {\n    fs::read_to_string(path).unwrap()\n}\n",
2597            ),
2598        ];
2599        assert_eq!(flagged(&files), vec!["project.rs"]);
2600    }
2601
2602    /// Glob chains re-reach ancestors transitively — grandparent binds `fs`, both
2603    /// hops glob-import `super::*` — and the `mod.rs` layout maps to the same module
2604    /// tree as the `name.rs` one. The middle file sees `fs` but never uses it, so
2605    /// only the leaf joins the (text-flagged) root.
2606    #[test]
2607    fn glob_super_resolution_is_transitive_across_mod_rs_parents() {
2608        let files = [
2609            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2610            ("a/mod.rs", "use super::*;\n\nmod b;\n"),
2611            (
2612                "a/b.rs",
2613                "use super::*;\n\nfn walk() {\n    let _ = fs::read_dir(\".\");\n}\n",
2614            ),
2615        ];
2616        assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
2617    }
2618
2619    /// A break anywhere in the chain stops resolution: the middle module does not
2620    /// glob-import `super::*`, so the leaf's `use super::*;` reaches a module with no
2621    /// `fs` binding to offer.
2622    #[test]
2623    fn a_break_in_the_glob_chain_stops_resolution() {
2624        let files = [
2625            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2626            ("a/mod.rs", "mod b;\n"),
2627            (
2628                "a/b.rs",
2629                "use super::*;\n\nfn walk() {\n    let _ = fs::read_dir(\".\");\n}\n",
2630            ),
2631        ];
2632        assert_eq!(flagged(&files), vec!["lib.rs"]);
2633    }
2634
2635    /// Nearest binding wins, as in Rust: the child re-binds `fs` to something that is
2636    /// not `std::fs`, so its bare `fs::` calls are that something's, not std's.
2637    #[test]
2638    fn a_local_non_std_binding_shadows_the_ancestors_std_fs() {
2639        let files = [
2640            ("lib.rs", "mod project;\n"),
2641            ("project.rs", "use std::fs;\n\nmod overlay;\nmod d;\n"),
2642            ("project/overlay.rs", "pub fn read(_p: &str) {}\n"),
2643            (
2644                "project/d.rs",
2645                "use super::*;\nuse crate::project::overlay as fs;\n\nfn f() {\n    let _ = fs::read(\"x\");\n}\n",
2646            ),
2647        ];
2648        assert_eq!(flagged(&files), vec!["project.rs"]);
2649    }
2650
2651    /// An aliased module binding resolves under its alias — the call site never
2652    /// contains the substring `fs::` at all.
2653    #[test]
2654    fn an_aliased_std_fs_binding_resolves_through_the_glob() {
2655        let files = [
2656            ("lib.rs", "mod p;\n"),
2657            ("p.rs", "use std::fs as stdfs;\n\nmod c;\n"),
2658            (
2659                "p/c.rs",
2660                "use super::*;\n\nfn f() {\n    stdfs::write(\"a\", \"b\").unwrap();\n}\n",
2661            ),
2662        ];
2663        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2664    }
2665
2666    /// `use std::{fs, io};` binds `fs` without ever containing the substring
2667    /// `std::fs` — the same blind spot as #1013's, one file deep. Resolution applies
2668    /// in the file's own scope, no glob import required.
2669    #[test]
2670    fn a_group_imported_fs_binding_is_resolved_in_its_own_file() {
2671        let src = "use std::{fs, io};\n\nfn f() -> io::Result<()> {\n    fs::metadata(\"x\").map(|_| ())\n}\n";
2672        assert!(
2673            !has_production_std_fs(src),
2674            "the text scan alone must miss it"
2675        );
2676        let files = [("thing.rs", src)];
2677        assert_eq!(flagged(&files), vec!["thing.rs"]);
2678    }
2679
2680    /// The item-import shape #1013 scope-checked (zero current instances), at the
2681    /// granularity this probe can reach: an ancestor's `use std::fs::File;` used as a
2682    /// bare path root `File::open` in a glob-importing child resolves and flags. (A
2683    /// bare *call* of an imported fn — `read_to_string(p)`, no `::` — presents no
2684    /// path root and remains out of a text-level scanner's reach, per the doc.)
2685    #[test]
2686    fn an_item_import_under_std_fs_resolves_as_a_path_root() {
2687        let files = [
2688            ("lib.rs", "mod p;\n"),
2689            ("p.rs", "use std::fs::File;\n\nmod c;\n"),
2690            (
2691                "p/c.rs",
2692                "use super::*;\n\nfn f() {\n    let _ = File::open(\"x\");\n}\n",
2693            ),
2694        ];
2695        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2696    }
2697
2698    /// A test module's `use super::*;` and tempdir `fs::` calls are test-scope — the
2699    /// `bynk-ide` files' shape (`architecture.rs`, `sequence.rs`), which must stay
2700    /// unflagged exactly as they were under the text-only scan.
2701    #[test]
2702    fn glob_and_bare_fs_inside_a_test_mod_stay_test_scope() {
2703        let files = [
2704            ("lib.rs", "use std::fs;\n\nmod w;\n"),
2705            (
2706                "w.rs",
2707                "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",
2708            ),
2709        ];
2710        assert_eq!(flagged(&files), vec!["lib.rs"]);
2711    }
2712
2713    /// The module-tree mapping behind the resolution, checked directly: `name.rs` and
2714    /// `mod.rs` layouts, a preferred `a.rs` over `a/mod.rs`, and rootless roots.
2715    #[test]
2716    fn module_parent_maps_both_file_layouts() {
2717        let files: Vec<(PathBuf, String)> = ["lib.rs", "a.rs", "a/b.rs", "c/mod.rs", "c/d.rs"]
2718            .iter()
2719            .map(|p| (PathBuf::from(p), String::new()))
2720            .collect();
2721        let idx = |name: &str| {
2722            files
2723                .iter()
2724                .position(|(p, _)| p == Path::new(name))
2725                .unwrap()
2726        };
2727        assert_eq!(module_parent(Path::new("lib.rs"), &files), None);
2728        assert_eq!(
2729            module_parent(Path::new("a.rs"), &files),
2730            Some(idx("lib.rs"))
2731        );
2732        assert_eq!(
2733            module_parent(Path::new("a/b.rs"), &files),
2734            Some(idx("a.rs"))
2735        );
2736        assert_eq!(
2737            module_parent(Path::new("c/mod.rs"), &files),
2738            Some(idx("lib.rs"))
2739        );
2740        assert_eq!(
2741            module_parent(Path::new("c/d.rs"), &files),
2742            Some(idx("c/mod.rs"))
2743        );
2744    }
2745
2746    // --- fs_below_driver: #1016 review findings ------------------------------
2747
2748    /// Finding 1: a `super::`-qualified path needs no glob import — module privacy is
2749    /// ancestor-scoped, so `super::fs` names the parent's private `use std::fs;` from
2750    /// any child. One disambiguating edit away from `discovery.rs:39`'s bare call,
2751    /// and it must not drop the file out of the count.
2752    #[test]
2753    fn a_super_qualified_path_resolves_without_a_glob_import() {
2754        let files = [
2755            ("lib.rs", "mod project;\n"),
2756            ("project.rs", "use std::fs;\n\nmod discovery;\n"),
2757            (
2758                "project/discovery.rs",
2759                "fn read_source(path: &std::path::Path) -> String {\n    super::fs::read_to_string(path).unwrap()\n}\n",
2760            ),
2761        ];
2762        assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
2763    }
2764
2765    /// Finding 1, the `crate::`-rooted form: the walk descends the module tree from
2766    /// the crate root file by file, then resolves the leaf against that module's
2767    /// bindings — from anywhere in the crate, glob import or not.
2768    #[test]
2769    fn a_crate_qualified_path_resolves_through_the_module_tree() {
2770        let files = [
2771            ("lib.rs", "mod other;\nmod project;\n"),
2772            (
2773                "other.rs",
2774                "fn f() {\n    let _ = crate::project::fs::read_dir(\".\");\n}\n",
2775            ),
2776            ("project.rs", "use std::fs;\n"),
2777        ];
2778        assert_eq!(flagged(&files), vec!["other.rs", "project.rs"]);
2779    }
2780
2781    /// Finding 1, stacked hops: `super::super::` climbs two parents (through a
2782    /// glob-free middle module — qualified paths don't need the glob chain).
2783    #[test]
2784    fn stacked_super_hops_climb_the_parent_chain() {
2785        let files = [
2786            ("lib.rs", "use std::fs;\n\nmod a;\n"),
2787            ("a/mod.rs", "mod b;\n"),
2788            (
2789                "a/b.rs",
2790                "fn f() {\n    let _ = super::super::fs::read_dir(\".\");\n}\n",
2791            ),
2792        ];
2793        assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
2794    }
2795
2796    /// Finding 1, `self::` composed with the glob chain: `self::fs` resolves in the
2797    /// file's own namespace, which includes what its `use super::*;` pulled in.
2798    #[test]
2799    fn a_self_qualified_path_resolves_through_the_files_own_glob_chain() {
2800        let files = [
2801            ("lib.rs", "mod p;\n"),
2802            ("p.rs", "use std::fs;\n\nmod c;\n"),
2803            (
2804                "p/c.rs",
2805                "use super::*;\n\nfn f() {\n    let _ = self::fs::read_dir(\".\");\n}\n",
2806            ),
2807        ];
2808        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2809    }
2810
2811    /// Finding 1's negatives: a qualified path to a name the parent binds to
2812    /// something other than `std::fs` stops at that binding, and a path through a
2813    /// module that doesn't exist resolves nowhere.
2814    #[test]
2815    fn a_qualified_path_to_a_non_std_binding_or_missing_module_is_not_flagged() {
2816        let files = [
2817            ("lib.rs", "mod overlay;\nmod p;\n"),
2818            ("overlay.rs", "pub fn read_dir(_p: &str) {}\n"),
2819            ("p.rs", "use crate::overlay as fs;\n\nmod d;\n"),
2820            (
2821                "p/d.rs",
2822                "fn f() {\n    let _ = super::fs::read_dir(\".\");\n    let _ = crate::missing::fs::read_dir(\".\");\n}\n",
2823            ),
2824        ];
2825        assert_eq!(flagged(&files), Vec::<String>::new());
2826    }
2827
2828    /// Finding 2: a locally-declared type-namespace item beats a glob-imported name
2829    /// in real Rust — a child with its own `mod fs;` calling `fs::…` is calling its
2830    /// own submodule, not the ancestor's `std::fs`.
2831    #[test]
2832    fn a_locally_declared_module_shadows_the_ancestors_std_fs() {
2833        let files = [
2834            ("lib.rs", "mod p;\n"),
2835            ("p.rs", "use std::fs;\n\nmod c;\n"),
2836            (
2837                "p/c.rs",
2838                "use super::*;\n\nmod fs;\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2839            ),
2840            ("p/c/fs.rs", "pub fn read_dir(_p: &str) {}\n"),
2841        ];
2842        assert_eq!(flagged(&files), vec!["p.rs"]);
2843    }
2844
2845    /// Finding 3: a trailing `//` comment on a `use` line must not sever the edge —
2846    /// neither the glob (`use super::*; // …`) nor the binding (`use std::fs; // …`).
2847    #[test]
2848    fn a_trailing_comment_on_a_use_line_does_not_sever_resolution() {
2849        let files = [
2850            ("lib.rs", "mod p;\n"),
2851            (
2852                "p.rs",
2853                "use std::fs; // read_source's disk fallback\n\nmod c;\n",
2854            ),
2855            (
2856                "p/c.rs",
2857                "use super::*; // parent's fs, PathBuf\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2858            ),
2859        ];
2860        assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
2861    }
2862
2863    /// Finding 4: the nested-group + `::self` normalisation branches, pinned
2864    /// directly — `use std::{fs::{self, File}, io};` binds `fs` *and* `File` to
2865    /// `std::fs`, and `io` only to the shadow set. Getting `::self` wrong would
2866    /// silently under-count, which is exactly this probe's failure mode.
2867    #[test]
2868    fn a_nested_group_with_self_binds_the_module_and_its_items() {
2869        let facts = fs_import_facts("use std::{fs::{self, File}, io};\n");
2870        let bound: Vec<&str> = facts.std_fs_bindings.iter().map(String::as_str).collect();
2871        assert_eq!(bound, vec!["File", "fs"]);
2872        assert!(facts.use_bound_names.contains("io"));
2873        assert!(!facts.std_fs_bindings.contains("io"));
2874    }
2875
2876    /// Finding 4, the children half of [`FsImportFacts`]' contract: a parent whose
2877    /// *only* `use std::fs;` lives in its `#[cfg(test)] mod` hands no binding to a
2878    /// glob-importing child — `bynk-ide/src/symbols.rs`' shape, latent until it
2879    /// grows a submodule.
2880    #[test]
2881    fn a_parents_test_mod_use_std_fs_does_not_reach_its_children() {
2882        let files = [
2883            ("lib.rs", "mod p;\n"),
2884            (
2885                "p.rs",
2886                "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",
2887            ),
2888            (
2889                "p/c.rs",
2890                "use super::*;\n\nfn f() {\n    let _ = fs::read_dir(\".\");\n}\n",
2891            ),
2892        ];
2893        assert_eq!(flagged(&files), Vec::<String>::new());
2894    }
2895
2896    // --- fs_below_driver: named-floor classification (#1104) -----------------
2897
2898    #[test]
2899    fn fn_name_on_line_strips_modifiers() {
2900        assert_eq!(fn_name_on_line("fn foo() {"), Some("foo".to_string()));
2901        assert_eq!(
2902            fn_name_on_line("pub(crate) fn read_adapter_binding("),
2903            Some("read_adapter_binding".to_string())
2904        );
2905        assert_eq!(
2906            fn_name_on_line("pub async unsafe fn go() {"),
2907            Some("go".to_string())
2908        );
2909    }
2910
2911    #[test]
2912    fn fn_name_on_line_ignores_non_fn_lines() {
2913        assert_eq!(fn_name_on_line("    let f = foo();"), None);
2914        assert_eq!(fn_name_on_line("/// calls fn bar somewhere"), None);
2915    }
2916
2917    /// A signature whose `{` arrives lines after the `fn` line — `read_adapter_binding`'s
2918    /// own real shape — must still resolve to the correct body range: `started` can't
2919    /// flip true on the parameter list, which has no braces of its own.
2920    #[test]
2921    fn production_fn_ranges_handles_a_wrapped_signature() {
2922        let src = "pub(crate) fn read_adapter_binding(\n    path: &Path,\n) -> std::io::Result<String> {\n    fs::read_to_string(path)\n}\n";
2923        let lines: Vec<&str> = src.lines().collect();
2924        let ranges = production_fn_ranges(&lines, &[]);
2925        assert_eq!(ranges.len(), 1);
2926        let (name, start, end) = &ranges[0];
2927        assert_eq!(name, "read_adapter_binding");
2928        assert_eq!(*start, 0);
2929        assert_eq!(*end, lines.len() - 1);
2930        assert_eq!(
2931            enclosing_fn(3, &ranges),
2932            Some("read_adapter_binding".to_string())
2933        );
2934    }
2935
2936    /// Build the `facts`/`parents` vectors [`file_is_named_fs_floor`] now takes as
2937    /// caller-supplied arguments, the same way [`fs_below_driver`] does, so each test
2938    /// below reads as "classify this file" rather than repeating the setup.
2939    fn classify(krate: &str, files: &[(PathBuf, String)], i: usize) -> bool {
2940        let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
2941        let parents: Vec<Option<usize>> =
2942            files.iter().map(|(p, _)| module_parent(p, files)).collect();
2943        file_is_named_fs_floor(krate, files, &facts, &parents, i)
2944    }
2945
2946    /// The concrete #1104 shape, in miniature: `project.rs`'s bare `use std::fs;` (no
2947    /// enclosing fn — never itself a violation) plus `discovery.rs`'s two named-exception
2948    /// functions. The whole file must read as a named floor, not residual.
2949    #[test]
2950    fn file_is_named_fs_floor_true_for_the_real_discovery_rs_shape() {
2951        let files = [
2952            (
2953                PathBuf::from("project.rs"),
2954                "use std::fs;\n\nmod discovery;\n".to_string(),
2955            ),
2956            (
2957                PathBuf::from("project/discovery.rs"),
2958                "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(),
2959            ),
2960        ];
2961        assert!(classify("bynk-emit", &files, 1));
2962    }
2963
2964    /// A new, unlisted fn touching `std::fs` in the *same file* as two named exceptions
2965    /// must flip the whole file to residual — no partial credit, since "named floor"
2966    /// must mean every touch is accounted for, not most of them.
2967    #[test]
2968    fn file_is_named_fs_floor_false_when_an_unnamed_fn_also_touches_fs() {
2969        let files = [
2970            (
2971                PathBuf::from("project.rs"),
2972                "use std::fs;\n\nmod discovery;\n".to_string(),
2973            ),
2974            (
2975                PathBuf::from("project/discovery.rs"),
2976                "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(),
2977            ),
2978        ];
2979        assert!(!classify("bynk-emit", &files, 1));
2980    }
2981
2982    /// A file whose only production-scope touch is a bare `use std::fs;` import — no
2983    /// enclosing fn at all — is trivially a named floor: the import performs no I/O by
2984    /// itself, and the descendant it enables is checked (and named) separately.
2985    #[test]
2986    fn file_is_named_fs_floor_true_for_an_import_only_file() {
2987        let files = [(
2988            PathBuf::from("project.rs"),
2989            "use std::fs;\n\nmod discovery;\n".to_string(),
2990        )];
2991        assert!(classify("bynk-emit", &files, 0));
2992    }
2993
2994    /// The same `discovery.rs` shape under the wrong crate label must not read as a
2995    /// floor — [`NAMED_FS_EXCEPTIONS`] is keyed on `(crate, file, fn)`, not `(file, fn)`
2996    /// alone, so a same-named file/fn pair in a different crate isn't accidentally
2997    /// covered.
2998    #[test]
2999    fn file_is_named_fs_floor_false_under_the_wrong_crate() {
3000        let files = [
3001            (
3002                PathBuf::from("project.rs"),
3003                "use std::fs;\n\nmod discovery;\n".to_string(),
3004            ),
3005            (
3006                PathBuf::from("project/discovery.rs"),
3007                "use super::*;\n\npub(crate) fn discover_bynk_files() {\n    let _ = fs::read_dir(\".\");\n}\n".to_string(),
3008            ),
3009        ];
3010        assert!(!classify("bynk-ide", &files, 1));
3011    }
3012
3013    /// Review finding (#1106): a module-scope `std::fs` touch that isn't an import
3014    /// declaration — a `static` initialiser doing real I/O — has no enclosing fn either,
3015    /// but is a genuine R2.3 violation and must not be waved through as a floor just
3016    /// because it sits outside every known fn range.
3017    #[test]
3018    fn file_is_named_fs_floor_false_for_a_module_scope_static_that_reads() {
3019        let files = [(
3020            PathBuf::from("project.rs"),
3021            "use std::fs;\n\nstatic ROOT: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| fs::read_to_string(\"x\").unwrap());\n"
3022                .to_string(),
3023        )];
3024        assert!(!classify("bynk-emit", &files, 0));
3025    }
3026
3027    /// Same review finding, the [`fn_name_on_line`] half: an `extern "C" fn` (a modifier
3028    /// combination the parser doesn't strip) produces no [`production_fn_ranges`] entry
3029    /// at all, so its whole body would fall into the "no enclosing fn" branch. It must
3030    /// still read as residual, not floor, once it touches `std::fs`.
3031    #[test]
3032    fn file_is_named_fs_floor_false_for_an_unparsed_extern_fn_body() {
3033        let files = [(
3034            PathBuf::from("project.rs"),
3035            "use std::fs;\n\nextern \"C\" fn callback() {\n    let _ = fs::read_dir(\".\");\n}\n"
3036                .to_string(),
3037        )];
3038        assert!(!classify("bynk-emit", &files, 0));
3039    }
3040
3041    // --- emit_abi_shapes (#999 Decision E) ----------------------------------
3042
3043    /// A binding's ordinary capability-interface imports, and the emit-ABI tag-layout
3044    /// names, must not be flagged — the exact failure mode Decision E rebuilt the probe
3045    /// to avoid (the original single-allowlist definition read 29-33 here, not 1).
3046    ///
3047    /// Exercises the real production allowlists via [`is_enumerated_emit_abi_or_capability_surface`]
3048    /// — not a local re-declaration. A test with its own copy of `EMIT_ABI` would still
3049    /// pass if the real one lost an entry (e.g. deleting `Uuid` from the production
3050    /// list), proving nothing about the probe it claims to cover.
3051    #[test]
3052    fn emit_abi_shapes_does_not_flag_capability_or_tag_layout_imports() {
3053        let src = "import type { Clock, Fetch, Locale } from \"./bynk.js\";\n\
3054                    import { FetchError, Uuid } from \"./bynk.js\";\n\
3055                    import { Err, None, Ok, Some, type Option, type Result } from \"./runtime.js\";\n";
3056        let imports = ts_named_imports_from_runtime_modules(src);
3057        let leaks: Vec<&String> = imports
3058            .iter()
3059            .filter(|i| !is_enumerated_emit_abi_or_capability_surface(i))
3060            .collect();
3061        assert!(leaks.is_empty(), "unexpected leaks: {leaks:?}");
3062    }
3063
3064    /// The falsifier from #999 Decision E, checked directly: deleting an entry from the
3065    /// real production allowlist must be detectable by *some* test — this one flags
3066    /// `Uuid` as a leak the moment it's removed from [`EMIT_ABI`], which the test above
3067    /// (using the real const) would also start failing on.
3068    #[test]
3069    fn is_enumerated_checks_the_real_production_allowlist() {
3070        assert!(is_enumerated_emit_abi_or_capability_surface("Uuid"));
3071        assert!(is_enumerated_emit_abi_or_capability_surface("LocaleTag"));
3072        assert!(!is_enumerated_emit_abi_or_capability_surface(
3073            "negotiateLocale"
3074        ));
3075    }
3076
3077    /// The real, current-tree finding this probe exists to surface: `negotiateLocale`,
3078    /// a plain value helper from `./runtime.js` alongside the tag-layout constructors,
3079    /// is neither an enumerated emit-ABI shape nor a capability-interface import.
3080    #[test]
3081    fn emit_abi_shapes_flags_a_non_enumerated_runtime_helper() {
3082        let src = "import { Err, None, Ok, Some, negotiateLocale, type Option, type Result } from \"./runtime.js\";\n";
3083        let imports = ts_named_imports_from_runtime_modules(src);
3084        assert!(imports.contains(&"negotiateLocale".to_string()));
3085    }
3086
3087    /// `FetchError` is `import type` in one binding and a plain value import in
3088    /// another (`FetchError.Timeout`) — Decision E's rejected type-vs-value
3089    /// discriminator. Confirms the extractor treats both forms as the same identifier,
3090    /// so the allowlist check doesn't depend on which form a given file happens to use.
3091    #[test]
3092    fn ts_import_extraction_ignores_type_only_vs_value_distinction() {
3093        let type_only = "import type { FetchError } from \"./bynk.js\";\n";
3094        let value = "import { FetchError, Uuid } from \"./bynk.js\";\n";
3095        assert_eq!(
3096            ts_named_imports_from_runtime_modules(type_only),
3097            vec!["FetchError".to_string()]
3098        );
3099        assert!(ts_named_imports_from_runtime_modules(value).contains(&"FetchError".to_string()));
3100    }
3101
3102    // --- options_sources -----------------------------------------------------
3103
3104    #[test]
3105    fn struct_body_finds_a_field_by_name() {
3106        let src = "struct Foo {\n    pub sources: Option<HashMap<PathBuf, String>>,\n    pub other: bool,\n}\n";
3107        let body = struct_body(src, "Foo").expect("struct body found");
3108        assert!(body.contains("sources"));
3109    }
3110
3111    #[test]
3112    fn struct_body_does_not_match_an_unrelated_struct() {
3113        let src =
3114            "struct Bar {\n    pub sources: bool,\n}\n\nstruct Foo {\n    pub other: bool,\n}\n";
3115        let body = struct_body(src, "Foo").expect("struct body found");
3116        assert!(!body.contains("sources"));
3117    }
3118
3119    // --- render_table's "Rules closed" section (#1001) ------------------------
3120
3121    fn empty_report() -> Report {
3122        Report { probes: Vec::new() }
3123    }
3124
3125    /// The section is static text — no count, no existence check — precisely
3126    /// because nothing regenerates `design/greenfield-status.md` when `stamp`
3127    /// writes the ledger, so a computed count would silently go stale the
3128    /// moment the first `closes_rule` landed (the drift a first draft of this
3129    /// section introduced, caught in #1001's review). This test pins "static"
3130    /// as the actual behaviour, not just the intent in a comment.
3131    #[test]
3132    fn render_table_rules_closed_section_is_static_regardless_of_the_tree() {
3133        let out = render_table(&empty_report());
3134        assert!(out.contains("greenfield-status-rules.md"), "{out}");
3135        assert!(
3136            out.contains("may not exist yet"),
3137            "the wording must not claim to know whether the ledger exists: {out}"
3138        );
3139    }
3140
3141    // --- ts_writes / ts_any (P7.0, #1296; testability + widening, review of #1297) --
3142
3143    /// Run [`ts_writes_violations`] over an in-memory file list — mirrors
3144    /// [`flagged`]'s own role for `production_std_fs_files`.
3145    fn ts_writes_over(files: &[(&str, &str)]) -> usize {
3146        let owned: Vec<(PathBuf, String)> = files
3147            .iter()
3148            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
3149            .collect();
3150        ts_writes_violations(&owned)
3151    }
3152
3153    /// Run [`ts_any_violations`] over an in-memory file list.
3154    fn ts_any_over(files: &[(&str, &str)]) -> usize {
3155        let owned: Vec<(PathBuf, String)> = files
3156            .iter()
3157            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
3158            .collect();
3159        ts_any_violations(&owned)
3160    }
3161
3162    /// Run [`verbatim_origins_violations`] over an in-memory file list.
3163    fn verbatim_origins_over(files: &[(&str, &str)]) -> usize {
3164        let owned: Vec<(PathBuf, String)> = files
3165            .iter()
3166            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
3167            .collect();
3168        verbatim_origins_violations(&owned)
3169    }
3170
3171    /// Run [`verbatim_sites_violations`] over an in-memory file list.
3172    fn verbatim_sites_over(files: &[(&str, &str)]) -> usize {
3173        let owned: Vec<(PathBuf, String)> = files
3174            .iter()
3175            .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
3176            .collect();
3177        verbatim_sites_violations(&owned)
3178    }
3179
3180    #[test]
3181    fn ts_writes_excluded_files_are_recognised() {
3182        assert!(is_ts_writes_excluded_file(Path::new("emitter/wrangler.rs")));
3183        assert!(is_ts_writes_excluded_file(Path::new("emitter/secrets.rs")));
3184        assert!(is_ts_writes_excluded_file(Path::new(
3185            "emitter/contracts.rs"
3186        )));
3187        assert!(is_ts_writes_excluded_file(Path::new(
3188            "emitter/source_map.rs"
3189        )));
3190        assert!(is_ts_writes_excluded_file(Path::new("testkit.rs")));
3191        // Name proximity to a file that used to be excluded must not false-positive:
3192        // `emitter/lower.rs` (the emitter's own lowering pass) is genuinely
3193        // TS-producing and must stay counted — unlike `ir/lower.rs` (the checker→IR
3194        // pass), which isn't a name-proximity risk at all any more: it left
3195        // `bynk-emit/src` entirely at Arc D's P7.12 crate carve.
3196        assert!(!is_ts_writes_excluded_file(Path::new("emitter/lower.rs")));
3197        assert!(!is_ts_writes_excluded_file(Path::new("ir/lower.rs")));
3198        assert!(!is_ts_writes_excluded_file(Path::new("emitter.rs")));
3199        assert!(!is_ts_writes_excluded_file(Path::new("project.rs")));
3200        assert!(!is_ts_writes_excluded_file(Path::new(
3201            "project/tests_emit.rs"
3202        )));
3203    }
3204
3205    /// Regression for a real mistake this slice's own grounding found: an earlier
3206    /// survey during phase 7's own track-opening research treated
3207    /// `project/tests_emit.rs` as excludable "test-assertion" noise. It is real
3208    /// production code (`process_tests`/`process_integration_tests`) per
3209    /// `semantics-in-the-checker.md`'s own settling finding for a different probe on
3210    /// the same file. Exercises the real probe, not just the predicate (review of
3211    /// #1297 — a first cut of this test called `is_ts_writes_excluded_file` directly,
3212    /// which can't catch a bug in [`ts_writes_violations`]'s own use of it).
3213    #[test]
3214    fn ts_writes_does_not_exclude_tests_emit_rs_wholesale() {
3215        let count = ts_writes_over(&[(
3216            "project/tests_emit.rs",
3217            "fn process_tests() {\n    let _ = format!(\"const x = 1;\");\n    let _ = writeln!(out, \"const y = 2;\");\n}\n",
3218        )]);
3219        assert_eq!(
3220            count, 2,
3221            "tests_emit.rs's own production emission code must be counted, not excluded wholesale"
3222        );
3223    }
3224
3225    #[test]
3226    fn is_path_construction_line_catches_the_idiom_not_ordinary_format_calls() {
3227        assert!(is_path_construction_line(
3228            "let p = PathBuf::from(format!(\"workers/{dashes}/index.ts\"));"
3229        ));
3230        assert!(is_path_construction_line(
3231            "root.join(format!(\"tests/integration_{sanitized}.test.ts\"))"
3232        ));
3233        assert!(is_path_construction_line(
3234            "output_path.with_file_name(format!(\"{name}.{suffix}\"))"
3235        ));
3236        // An ordinary TS-producing `format!` call, no path construction, must not be
3237        // excluded by this idiom.
3238        assert!(!is_path_construction_line(
3239            "writeln!(out, \"{}\", format!(\"const {name} = 1;\"))"
3240        ));
3241    }
3242
3243    #[test]
3244    fn line_violates_ts_any_catches_the_cast_and_the_bare_annotation() {
3245        assert!(line_violates_ts_any("let x = (value as any).field;"));
3246        assert!(line_violates_ts_any("format!(\"{}: any\", name)"));
3247        assert!(line_violates_ts_any(
3248            "\"(seq: any[]) => ({ns} as any).drive(seq)\""
3249        ));
3250        assert!(!line_violates_ts_any("let x: unknown = value;"));
3251    }
3252
3253    /// Regression for review of #1297, finding 1: `any` in generic type-argument
3254    /// position (`Record<string, any[]>`, the live `emitter/lower.rs`
3255    /// `joinOn`/`leftJoin`/`groupBy` shape) contains neither `as any` nor `: any` and
3256    /// was silently uncounted by the round-one predicate.
3257    #[test]
3258    fn line_violates_ts_any_catches_generic_position_any() {
3259        assert!(line_violates_ts_any(
3260            "\"{{ const __h: Record<string, any[]> = {{}}; ...}}\""
3261        ));
3262        assert!(line_violates_ts_any("\"Array<any>\""));
3263        assert!(line_violates_ts_any("\"Promise<any>\""));
3264        // Must not regress the round-one patterns while widening.
3265        assert!(line_violates_ts_any("(value as any).field"));
3266        assert!(line_violates_ts_any("(e: any) => {}"));
3267    }
3268
3269    /// Regression for review of #1322, finding 2: once a site builds a real
3270    /// `bynk_ts::TsType` node instead of writing TypeScript text directly, the
3271    /// emitted `any` no longer appears as Rust-source `as any`/`: any` — the round-
3272    /// one/round-two patterns above all match *emitted-text* spellings, none of
3273    /// which appear in `TsType::named("any")`. `workers.rs`'s own three real sites
3274    /// (#1321) were silently uncounted until this pattern was added.
3275    #[test]
3276    fn line_violates_ts_any_catches_the_named_any_construction_spelling() {
3277        assert!(line_violates_ts_any(
3278            "    let mut args = vec![as_expr(ident(\"payload\"), TsType::named(\"any\"))];"
3279        ));
3280        assert!(line_violates_ts_any(
3281            "        Some(TsType::named(\"any\")),"
3282        ));
3283        // Must not regress the round-one/round-two patterns while widening.
3284        assert!(line_violates_ts_any("(value as any).field"));
3285        assert!(line_violates_ts_any("\"Array<any>\""));
3286    }
3287
3288    /// A comment mentioning either pattern in prose — the same self-reference-shaped
3289    /// hazard [`bynk_dotted_literals`]'s own regression tests guard against for a
3290    /// different probe — must not count.
3291    #[test]
3292    fn line_violates_ts_any_ignores_comments() {
3293        assert!(!line_violates_ts_any(
3294            "/// lowering machinery, same as any other subexpression."
3295        ));
3296        assert!(!line_violates_ts_any(
3297            "// TODO: stop emitting `: any` here once bynk-ts exists"
3298        ));
3299    }
3300
3301    /// `#[cfg(test)]`-gated write!-family calls (a file's own unit tests constructing a
3302    /// fixture string) must not count toward either probe — mirrors
3303    /// [`has_production_std_fs`]'s own test-range exclusion for a different probe.
3304    /// Exercises the real probes end to end, not a re-implementation of their loop
3305    /// (review of #1297, finding 2): deleting either probe's `in_test_range` guard, its
3306    /// `is_ts_writes_excluded_file` `continue`, or (for `ts_writes`) its
3307    /// `is_path_construction_line` `continue` now fails one of these tests.
3308    #[test]
3309    fn ts_writes_and_ts_any_exclude_cfg_test_ranges() {
3310        let src = "fn production() {\n    let _ = format!(\"const x = 1;\");\n}\n\n\
3311                    #[cfg(test)]\nmod tests {\n    #[test]\n    fn t() {\n        \
3312                    let _ = format!(\"(v as any)\");\n    }\n}\n";
3313        assert_eq!(
3314            ts_writes_over(&[("emitter.rs", src)]),
3315            1,
3316            "only the production format! call counts"
3317        );
3318        assert_eq!(
3319            ts_any_over(&[("emitter.rs", src)]),
3320            0,
3321            "the test-only `as any` site must be excluded"
3322        );
3323    }
3324
3325    /// Exercises the real probes' file-exclusion `continue`, not just the predicate:
3326    /// a whole file on [`TS_WRITES_EXCLUDED_FILES`] must contribute 0 to either count
3327    /// even when its content would otherwise match both.
3328    #[test]
3329    fn ts_writes_and_ts_any_exclude_named_non_ts_files_end_to_end() {
3330        let files = [(
3331            "emitter/wrangler.rs",
3332            "fn write_toml(out: &mut String) {\n    let _ = writeln!(out, \"name = {v}\");\n    let __x: any = 1;\n}\n",
3333        )];
3334        assert_eq!(ts_writes_over(&files), 0);
3335        assert_eq!(ts_any_over(&files), 0);
3336    }
3337
3338    /// Exercises the real probes' [`is_path_construction_line`] `continue` end to end,
3339    /// not just the predicate in isolation.
3340    #[test]
3341    fn ts_writes_excludes_path_construction_end_to_end() {
3342        let files = [(
3343            "project.rs",
3344            "fn out_path(dashes: &str) -> PathBuf {\n    PathBuf::from(format!(\"workers/{dashes}/index.ts\"))\n}\n\nfn emit(out: &mut String) {\n    let _ = writeln!(out, \"export const x = 1;\");\n}\n",
3345        )];
3346        assert_eq!(
3347            ts_writes_over(&files),
3348            1,
3349            "the path-construction line must not count; the genuine emission line must"
3350        );
3351    }
3352
3353    #[test]
3354    fn verbatim_origins_counts_distinct_variants_not_construction_sites() {
3355        let files = [(
3356            "emitter/contracts.rs",
3357            "fn a() { TsStmt::verbatim(VerbatimOrigin::Contracts, \"x\", None) }\nfn b() { TsStmt::verbatim(VerbatimOrigin::Contracts, \"y\", None) }\nfn c() { TsStmt::verbatim(VerbatimOrigin::Secrets, \"z\", None) }\n",
3358        )];
3359        // Three construction sites, but only two distinct origins.
3360        assert_eq!(verbatim_origins_over(&files), 2);
3361        assert_eq!(verbatim_sites_over(&files), 3);
3362    }
3363
3364    #[test]
3365    fn verbatim_origins_and_sites_ignore_comments() {
3366        let files = [(
3367            "emitter/contracts.rs",
3368            "// TsStmt::verbatim(VerbatimOrigin::Contracts, \"x\", None)\n/// Mentions VerbatimOrigin::Secrets in prose.\n",
3369        )];
3370        assert_eq!(verbatim_origins_over(&files), 0);
3371        assert_eq!(verbatim_sites_over(&files), 0);
3372    }
3373
3374    #[test]
3375    fn verbatim_origins_and_sites_read_zero_over_an_empty_tree() {
3376        let files: [(&str, &str); 0] = [];
3377        assert_eq!(verbatim_origins_over(&files), 0);
3378        assert_eq!(verbatim_sites_over(&files), 0);
3379    }
3380
3381    /// Review of #1308, finding 6: without stripping `#[cfg(test)]` ranges,
3382    /// a single `bynk-emit` unit test fixture constructing a `TsStmt::
3383    /// verbatim(...)` for its own coverage would pin `verbatim_sites` above
3384    /// its documented 0 floor permanently, for a reason unrelated to
3385    /// residual production emission.
3386    #[test]
3387    fn verbatim_origins_and_sites_exclude_cfg_test_ranges() {
3388        let src = "fn production() {\n    TsStmt::verbatim(VerbatimOrigin::Contracts, \"x\", None);\n}\n\n\
3389                    #[cfg(test)]\nmod tests {\n    #[test]\n    fn t() {\n        \
3390                    TsStmt::verbatim(VerbatimOrigin::Secrets, \"y\", None);\n    }\n}\n";
3391        assert_eq!(
3392            verbatim_origins_over(&[("emitter/contracts.rs", src)]),
3393            1,
3394            "only the production-code origin counts"
3395        );
3396        assert_eq!(
3397            verbatim_sites_over(&[("emitter/contracts.rs", src)]),
3398            1,
3399            "the test-only construction site must be excluded"
3400        );
3401    }
3402}