xtask/greenfield_status.rs
1//! `cargo xtask greenfield-status` — the probe harness (track doc §8, proposal #999).
2//!
3//! Twenty-one 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), phase 7's own four — `ts_writes`, `ts_any`,
7//! `verbatim_origins`, `verbatim_sites` (P7.0/#1296, P7.5/#1307 — see phase 7's own
8//! closing summary in `design/archive/retired-tracks.md`) — and phase 8's own two,
9//! `incremental_query_types` and `keystroke_latency` (P8.0/#1510, settled by #1509's
10//! Q5/ADR 0414 — see `design/tracks/incrementality.md` §5), plus the IR cutover
11//! track's own adoption probe, `unconsumed_ir_items` (Slice D3 of #1542 — the gate the
12//! 30 August 2026 post-restructuring review's Part 5 §8 asked for), and the
13//! runtime-semantics track's `diagnostic_coverage` (#1662, G2 of #1648).
14//!
15//! **Sixteen are gated**, committed and diffed: `workspace_lints`, `fs_below_driver`,
16//! `options_sources`, `hoist_sinks`, `span_keyed_maps`, `emit_diagnostics`,
17//! `ide_emit_edge`, `ast_importers`, `emit_abi_shapes`, `ts_writes`, `ts_any`,
18//! `verbatim_origins`, `verbatim_sites`, `incremental_query_types`,
19//! `unconsumed_ir_items`, `diagnostic_coverage`. Ten of these are
20//! zero/closure-shaped — a boolean, or a count pinned at a small, argued floor
21//! (`ast_importers` = 5, `emit_abi_shapes` = 1). Phase 7's own four are the same shape:
22//! each converged toward an argued floor over dozens of slices, the same trajectory
23//! `ast_importers` had throughout phase 6's 59 — retired at `ts_writes` = 809,
24//! `ts_any` = 26, `verbatim_origins` = 1, `verbatim_sites` = 2 (phase 7's own closing
25//! summary, `design/archive/retired-tracks.md`), none the literal 0 first proposed —
26//! gated throughout despite the churn that implies, a deliberate call argued in ADR
27//! 0389/ADR 0390 (review of #1297), not an oversight of #999 Decision D's
28//! churn-avoidance principle. `incremental_query_types` is a different shape again —
29//! phase 8's own probe reads a one-time existence proof, not a count trending toward a
30//! floor (settled §5/Q5, ADR 0414), re-settled by #1537 to certify that the two
31//! levels that never found a consumer stay deleted; see its own doc comment. A
32//! disagreement between a
33//! fresh run and the committed table fails `greenfield_status_table_is_current`
34//! (`xtask/tests/greenfield_status.rs`), which rides both the `test` job (`cargo test
35//! --workspace`, any Rust-touching PR) and the `drift` job's existing `cargo test -p
36//! xtask` (pending/decisions-only PRs) — no new CI wiring (#999 Decision D, which also
37//! explains why a `drift`-job *step* would have been silently skipped on the PRs that
38//! move these probes most).
39//!
40//! A disagreement a pending file acknowledges (its `probes` key) does not fail the
41//! gate: the increment leaves the shared table alone and `cargo xtask stamp --apply`
42//! rewrites the gated rows on `main` in merge order ([`refresh_gated_rows`]). The
43//! acknowledgement still has to be written, and reviewed, in the PR — a gated probe
44//! still cannot move silently — but parallel PRs no longer conflict on these rows.
45//!
46//! **Five are count/ratio trend probes**, recomputed and printed but never diffed:
47//! `wildcard_arms`, `keep_in_sync`, `test_density`, `fixture_kinds`,
48//! `keystroke_latency`. The first four move on nearly any ordinary Rust PR with no
49//! slice actively driving them toward a floor (§8 calls two of them "trends, not
50//! gates"); hard-gating them would make the committed table churn, and conflict, on
51//! routine work for no corresponding benefit. `keystroke_latency` moves on nothing
52//! yet — settled (Q3/ADR 0414) as staying "not measured" for phase 8's own whole
53//! lifetime, since no scheduler ships this phase to produce a real number; reported
54//! anyway so the trajectory's own §3.0 baseline has a live, CI-computed row.
55//!
56//! `Closes-Rule:` rule-id provenance (#999 Decision B) is deferred to a follow-on
57//! slice — the committed table below carries no rule-citation column yet.
58
59use std::collections::BTreeSet;
60use std::fmt::Write as _;
61use std::path::{Path, PathBuf};
62use std::process::Command;
63
64/// One probe's result. `gated` probes are diffed against the committed table by
65/// [`crate::greenfield_status::gated_disagreements`]; the rest are reported only.
66pub struct Probe {
67 pub name: &'static str,
68 pub gated: bool,
69 pub reads: String,
70}
71
72pub struct Report {
73 pub probes: Vec<Probe>,
74}
75
76impl Report {
77 pub fn get(&self, name: &str) -> &str {
78 self.probes
79 .iter()
80 .find(|p| p.name == name)
81 .map(|p| p.reads.as_str())
82 .unwrap_or_else(|| panic!("no probe named {name:?}"))
83 }
84}
85
86/// Run every probe against the tree rooted at `root` (the repo root). Used by the CLI's
87/// full report; the gating test uses the sixteen gated probes alone
88/// ([`gated_disagreements`]) so it never pays for a workspace-wide clippy pass
89/// (`wildcard_arms`) just to check the probes that are actually diffed.
90pub fn run(root: &Path) -> Report {
91 let mut probes = run_gated(root);
92 probes.extend(run_trend(root));
93 Report { probes }
94}
95
96/// The gated probes' names, in [`run_gated`]'s order — what a pending file's
97/// `probes` key may cite. `greenfield_status_table_is_current` asserts this
98/// matches the probes [`run_gated`] actually returns, so the two cannot drift.
99pub const GATED_PROBES: [&str; 16] = [
100 "workspace_lints",
101 "fs_below_driver",
102 "options_sources",
103 "hoist_sinks",
104 "span_keyed_maps",
105 "emit_diagnostics",
106 "ide_emit_edge",
107 "ast_importers",
108 "emit_abi_shapes",
109 "ts_writes",
110 "ts_any",
111 "verbatim_origins",
112 "verbatim_sites",
113 "incremental_query_types",
114 "unconsumed_ir_items",
115 "diagnostic_coverage",
116];
117
118/// The sixteen gated (zero/closure) probes only — what [`gated_disagreements`] diffs.
119pub fn run_gated(root: &Path) -> Vec<Probe> {
120 vec![
121 workspace_lints(root),
122 fs_below_driver(root),
123 options_sources(root),
124 hoist_sinks(root),
125 span_keyed_maps(root),
126 emit_diagnostics(root),
127 ide_emit_edge(root),
128 ast_importers(root),
129 emit_abi_shapes(root),
130 ts_writes(root),
131 ts_any(root),
132 verbatim_origins(root),
133 verbatim_sites(root),
134 incremental_query_types(root),
135 unconsumed_ir_items(root),
136 diagnostic_coverage(root),
137 ]
138}
139
140/// The five reported-only trend probes — never diffed, and notably including the one
141/// (`wildcard_arms`) that shells out to a full `cargo clippy --workspace` pass, which
142/// the gating test must not pay for on every run.
143fn run_trend(root: &Path) -> Vec<Probe> {
144 vec![
145 wildcard_arms(root),
146 keep_in_sync(root),
147 test_density(root),
148 fixture_kinds(root),
149 keystroke_latency(root),
150 ]
151}
152
153/// `design/greenfield-status.md` — the committed table this probe set regenerates.
154pub fn table_path(root: &Path) -> PathBuf {
155 root.join("design/greenfield-status.md")
156}
157
158// --- Filesystem helpers --------------------------------------------------
159
160/// Every `.rs` file under `dir`, recursively, as `(path, contents)`. Unreadable files
161/// (permissions, non-UTF-8) are skipped rather than failing the whole walk — this is a
162/// measurement tool, not a build step.
163fn rust_files(dir: &Path) -> Vec<(PathBuf, String)> {
164 let mut out = Vec::new();
165 walk(dir, &mut out);
166 out
167}
168
169fn walk(dir: &Path, out: &mut Vec<(PathBuf, String)>) {
170 let Ok(entries) = std::fs::read_dir(dir) else {
171 return;
172 };
173 let mut entries: Vec<_> = entries.flatten().collect();
174 entries.sort_by_key(|e| e.file_name());
175 for entry in entries {
176 let path = entry.path();
177 if path.is_dir() {
178 walk(&path, out);
179 } else if path.extension().is_some_and(|e| e == "rs")
180 && let Ok(contents) = std::fs::read_to_string(&path)
181 {
182 out.push((path, contents));
183 }
184 }
185}
186
187/// The inner text of every **standalone** `"bynk.<ident>"` string literal (the
188/// `bynk.*` convention used for diagnostic codes and commons/namespace paths alike).
189///
190/// Standalone, not merely prefix-matching: the identifier run must be immediately
191/// followed by the closing quote, matching the naive `rg -o '"bynk\.[a-zA-Z0-9_.]*"'`
192/// this probe is deliberately more careful than (#999 Decision A). Without that
193/// requirement this would also match the *start* of an unrelated, longer message that
194/// merely happens to begin with "bynk." — e.g. a panic string
195/// `"bynk.map itself uses bynk.list, so list must be injected too: {paths:?}"` is prose
196/// beginning with a namespace-shaped word, not a `"bynk.map"` code literal, and a
197/// dev-only compile-time error message split across lines with a `\`-continuation
198/// (`"bynk.emit.unresolved_cross_context_signature: no signature for \` ...) is one
199/// string, not a diagnostic-code literal, even though its first segment matches the
200/// identifier charset. Both were found — and wrongly counted — by an earlier,
201/// less careful version of this scan; the fix is requiring the closing quote.
202fn bynk_dotted_literals(src: &str) -> Vec<&str> {
203 let mut out = Vec::new();
204 let bytes = src.as_bytes();
205 let mut i = 0;
206 while let Some(rel) = src[i..].find("\"bynk.") {
207 let start = i + rel + 1; // skip the opening quote
208 let mut end = start;
209 while end < bytes.len()
210 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'.')
211 {
212 end += 1;
213 }
214 if end < bytes.len() && bytes[end] == b'"' {
215 out.push(&src[start..end]);
216 }
217 i = end.max(start + 1);
218 }
219 out
220}
221
222/// True if `line`, trimmed, is a `//` or `///` or `//!` line comment. Doesn't attempt
223/// block comments (`/* */`) — none of this codebase's `bynk.*`/dead-identifier
224/// mentions live in one.
225fn is_line_comment(line: &str) -> bool {
226 line.trim_start().starts_with("//")
227}
228
229// --- Gated probe 1: workspace_lints --------------------------------------
230
231/// R2.12. `[workspace.lints]` presence and `clippy::wildcard_enum_match_arm`'s level in
232/// the root `Cargo.toml`. A boolean-shaped probe (not a count) — gated because it only
233/// ever changes once, when T0.3 lands it.
234fn workspace_lints(root: &Path) -> Probe {
235 let cargo_toml = std::fs::read_to_string(root.join("Cargo.toml")).unwrap_or_default();
236 let has_section = cargo_toml
237 .lines()
238 .any(|l| l.trim() == "[workspace.lints.clippy]" || l.trim() == "[workspace.lints]");
239 let level = cargo_toml
240 .lines()
241 .find(|l| l.contains("wildcard_enum_match_arm"))
242 .map(|l| l.trim().to_string());
243 let reads = match (has_section, level) {
244 (true, Some(l)) => format!("present — {l}"),
245 (true, None) => "present, wildcard_enum_match_arm not set".to_string(),
246 (false, _) => "absent".to_string(),
247 };
248 Probe {
249 name: "workspace_lints",
250 gated: true,
251 reads,
252 }
253}
254
255// --- Gated probe 2: fs_below_driver --------------------------------------
256
257/// R2.3. Files under `bynk-emit/src`, `bynk-ide/src`, `bynk-fmt/src` (the crates below
258/// the `bynk` driver, which owns disk I/O) that touch `std::fs` in **production** code.
259///
260/// Excludes usage inside a trailing `#[cfg(test)] mod tests { ... }` block — the
261/// convention every file in this codebase uses, always the last item in the file. A
262/// line is production-scope unless it falls at or after the line following a
263/// `#[cfg(test)]` attribute whose very next non-empty line opens a `mod ... {` block
264/// (as opposed to a `mod name;` external-file declaration, which is not a scope at
265/// all). This mirrors the comment-exclusion discipline elsewhere in this probe set:
266/// tests writing fixtures to a tempdir are not "the driver's job" bypassed, and
267/// counting them would report a rule open that the production code has already closed.
268///
269/// A file counts if its own text names `std::fs` ([`has_production_std_fs`]), **or** if
270/// a bare `fs::`-style call site in it resolves to `std::fs` through its imports
271/// ([`production_std_fs_files`]) — a module-level `use std::fs;` in a parent module is
272/// visible to a child through `use super::*;` (module privacy is ancestor-scoped), so
273/// `bynk-emit/src/project/discovery.rs` reads and walks the filesystem while never
274/// spelling `std::fs` itself. The literal text scan alone missed exactly that file,
275/// so a probe reading `bynk-emit=0` would have asserted R2.3 closed on a false
276/// premise (#1013).
277///
278/// #1104 (a content-ownership (#1086) probe-precision follow-on): a flagged *count*
279/// alone can't tell a residual R2.3 violation from a documented, permanent exception —
280/// `bynk-emit` read 3 that way from the track's retirement (`design/archive/
281/// retired-tracks.md`'s closing summary) until P4.0 moved all three named files out of
282/// `bynk-emit` entirely, leaving [`NAMED_FS_EXCEPTIONS`] empty (#1561) — the mechanism
283/// stays live for whatever named exception is decided next. So each flagged file is
284/// additionally classified as a **named floor** file — every
285/// production-scope touch it has is either inside one of those named functions, or is
286/// a bare import declaration (no fn encloses it — [`enclosing_fn`] returns `None`) that
287/// performs no I/O of its own, existing only so a *descendant* module's bare `fs::`
288/// call can resolve (the motivating case, #1013: `project.rs`'s own `use std::fs;`,
289/// which `discovery.rs` and `paths.rs` glob-imported via `use super::*;` before P4.0
290/// moved both files out) — or a **residual** file: any other file
291/// touching `std::fs` in production scope, which still reads as a real R2.3 violation
292/// ([`file_is_named_fs_floor`]).
293fn fs_below_driver(root: &Path) -> Probe {
294 let crates = ["bynk-emit", "bynk-ide", "bynk-fmt"];
295 let mut per_crate = Vec::new();
296 let mut total = 0usize;
297 let mut total_floor = 0usize;
298 for krate in crates {
299 let dir = root.join(krate).join("src");
300 let files: Vec<(PathBuf, String)> = rust_files(&dir)
301 .into_iter()
302 .map(|(path, contents)| {
303 let rel = path.strip_prefix(&dir).unwrap_or(&path).to_path_buf();
304 (rel, contents)
305 })
306 .collect();
307 let flagged = production_std_fs_files(&files);
308 let count = flagged.len();
309 total += count;
310 let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
311 let parents: Vec<Option<usize>> = files
312 .iter()
313 .map(|(p, _)| module_parent(p, &files))
314 .collect();
315 let floor = flagged
316 .iter()
317 .filter(|&&i| {
318 file_is_named_fs_floor(krate, &files, &facts, &parents, i, NAMED_FS_EXCEPTIONS)
319 })
320 .count();
321 total_floor += floor;
322 let residual = count - floor;
323 per_crate.push(if floor > 0 {
324 format!("{krate}={count} ({floor} named floor, {residual} residual)")
325 } else {
326 format!("{krate}={count}")
327 });
328 }
329 Probe {
330 name: "fs_below_driver",
331 gated: true,
332 reads: format!(
333 "{total} files ({}) — {total_floor} named floor, {} residual total",
334 per_crate.join(", "),
335 total - total_floor
336 ),
337 }
338}
339
340/// #1104: the specific, permanently-carved-out production functions whose
341/// `std::fs` touch is a *named* exception, not evidence of unfinished R2.3
342/// migration — settled in `design/tracks/content-ownership.md` §3.2 (retired) and
343/// its closing summary in `design/archive/retired-tracks.md`. `(crate, file path
344/// relative to that crate's `src/`, enclosing production fn name)`. A future
345/// carve-out decided the same deliberate way joins this list; anything touching
346/// `std::fs` in production scope that isn't listed here reads as a residual R2.3
347/// violation, per [`file_is_named_fs_floor`].
348///
349/// Empty since #1561: these three entries were real when #1104 (`769a60a3`,
350/// 6 Aug 2026) added them, but P4.0 (`69af8f2d`, the very next day) moved
351/// `discover_bynk_files`/`read_adapter_binding` (`project/discovery.rs`) and
352/// `try_read_project_paths` (`project/paths.rs`) out of `bynk-emit` into
353/// `bynk-project` entirely — dead from that point on, unnoticed for weeks.
354/// `fs_below_driver`'s `bynk-emit` reading was already 0 with them present
355/// (nothing in the tree matched the dead tuples), confirmed unchanged with
356/// them gone.
357const NAMED_FS_EXCEPTIONS: &[(&str, &str, &str)] = &[];
358
359/// Is flagged file `files[i]` (already known, by [`production_std_fs_files`], to touch
360/// `std::fs` in production scope) a **named floor** file — every production-scope touch
361/// it has is either inside a [`NAMED_FS_EXCEPTIONS`] function for this exact
362/// `(krate, file)`, or a bare `use` import declaration (which reads but performs no
363/// filesystem operation by itself, unlike a module-scope `static`/`const` initialiser or
364/// macro invocation that might)? `facts`/`parents` are the caller's already-computed
365/// [`fs_import_facts`]/[`module_parent`] vectors for `files`, threaded through rather
366/// than recomputed per flagged file.
367///
368/// A single disallowed touch — inside an unlisted fn, inside a listed fn's *file* but
369/// wrong *name*, or outside every fn and not a plain import — makes the whole file
370/// residual: partial credit isn't meaningful here, since the point is "can a reader stop
371/// cross-referencing track docs for this file," not a ratio. Likewise, a file this
372/// function attributes *no* touch line to at all (despite the caller already knowing it's
373/// flagged — [`line_touches_std_fs`]'s re-implementation of the file-level detection
374/// disagreeing with it) reads as residual, not floor: an unattributable touch means this
375/// classifier doesn't understand the file, which must fail loud, not quiet.
376///
377/// `exceptions` is [`NAMED_FS_EXCEPTIONS`] at the one production call site
378/// ([`fs_below_driver`]) — parameterised (#1561) so a test can exercise the
379/// matching logic against a synthetic tuple instead of real, currently-empty
380/// production data, the same way `facts`/`parents` are caller-supplied rather
381/// than recomputed.
382fn file_is_named_fs_floor(
383 krate: &str,
384 files: &[(PathBuf, String)],
385 facts: &[FsImportFacts],
386 parents: &[Option<usize>],
387 i: usize,
388 exceptions: &[(&str, &str, &str)],
389) -> bool {
390 let (path, _) = &files[i];
391 let rel = path.to_string_lossy().replace('\\', "/");
392 let lines: Vec<&str> = files[i].1.lines().collect();
393 let ranges = test_mod_ranges(&lines);
394 let fn_ranges = production_fn_ranges(&lines, &ranges);
395
396 let mut saw_touch = false;
397 for (li, line) in lines.iter().enumerate() {
398 if in_test_range(li, &ranges) {
399 continue;
400 }
401 if !line_touches_std_fs(i, line, facts, parents, files) {
402 continue;
403 }
404 saw_touch = true;
405 let Some(fn_name) = enclosing_fn(li, &fn_ranges) else {
406 // No enclosing fn is harmless only when the line is literally an import
407 // declaration. A module-scope `static`/`const` initialiser, a macro
408 // invocation, or a fn shape `fn_name_on_line` can't parse (`extern "C" fn`)
409 // does real I/O outside every known range and must read as residual.
410 if use_declaration(line).is_some() {
411 continue;
412 }
413 return false;
414 };
415 let named = exceptions
416 .iter()
417 .any(|&(c, f, func)| c == krate && f == rel && func == fn_name);
418 if !named {
419 return false;
420 }
421 }
422 saw_touch
423}
424
425/// Does `line` (already known to be production-scope) itself touch `std::fs` — by the
426/// same two means [`production_std_fs_files`] checks at file granularity, applied here
427/// to one line: a literal `std::fs` substring, or a bare/qualified path this line spells
428/// that resolves to `std::fs` through file `i`'s visible import bindings.
429fn line_touches_std_fs(
430 i: usize,
431 line: &str,
432 facts: &[FsImportFacts],
433 parents: &[Option<usize>],
434 files: &[(PathBuf, String)],
435) -> bool {
436 if line.contains("std::fs") {
437 return true;
438 }
439 let mut roots = BTreeSet::new();
440 collect_bare_path_roots(line, &mut roots);
441 if roots.iter().any(|name| {
442 matches!(
443 resolve_name_in_module(i, name, facts, parents),
444 NameResolution::StdFs
445 )
446 }) {
447 return true;
448 }
449 let mut chains = BTreeSet::new();
450 collect_qualified_paths(line, &mut chains);
451 chains
452 .iter()
453 .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
454}
455
456/// The name and inclusive body line-range of every production-scope `fn` in `lines`
457/// (`test_ranges` excluded, same as everywhere else in this probe) — used by
458/// [`file_is_named_fs_floor`] to attribute a flagged touch line to its enclosing
459/// function. A wrapped signature (the `{` arriving lines after the `fn` line, past a
460/// multi-line parameter list) is handled the same way [`test_mod_ranges`] handles a
461/// `mod` line: brace depth is tracked starting at the `fn` line itself, but a parameter
462/// list has no `{`/`}` in it, so `started` only flips true once the real body-opening
463/// brace arrives, however many lines later.
464fn production_fn_ranges(
465 lines: &[&str],
466 test_ranges: &[(usize, usize)],
467) -> Vec<(String, usize, usize)> {
468 let mut out = Vec::new();
469 for (i, line) in lines.iter().enumerate() {
470 if in_test_range(i, test_ranges) {
471 continue;
472 }
473 let Some(name) = fn_name_on_line(line) else {
474 continue;
475 };
476 let mut state = BraceScanState::Normal;
477 let mut depth = 0i32;
478 let mut started = false;
479 let mut end = lines.len() - 1;
480 for (j, l) in lines[i..].iter().enumerate() {
481 let (delta, new_state) = brace_delta(l, state);
482 state = new_state;
483 depth += delta;
484 if depth != 0 {
485 started = true;
486 }
487 if started && depth == 0 {
488 end = i + j;
489 break;
490 }
491 }
492 out.push((name, i, end));
493 }
494 out
495}
496
497/// The leading `fn NAME` on `line`, past an optional `pub`/`pub(...)`, `async`,
498/// `unsafe`, `const` modifier run (in any order/repetition, mirroring
499/// [`collect_declared_type_name`]'s `pub`-stripping) — `None` if `line` doesn't open a
500/// function at all (a call site, a doc comment mentioning "fn", a closure). Doesn't
501/// require a trailing `{` or even `(` on this same line — a wrapped signature's `fn`
502/// line can end right at the name.
503fn fn_name_on_line(line: &str) -> Option<String> {
504 let mut t = line.trim();
505 loop {
506 if let Some(rest) = t.strip_prefix("pub") {
507 let rest = rest.trim_start();
508 t = if let Some(after_paren) = rest.strip_prefix('(') {
509 after_paren.split_once(')')?.1.trim_start()
510 } else {
511 rest
512 };
513 continue;
514 }
515 let mut advanced = false;
516 for kw in ["async ", "unsafe ", "const "] {
517 if let Some(rest) = t.strip_prefix(kw) {
518 t = rest.trim_start();
519 advanced = true;
520 break;
521 }
522 }
523 if !advanced {
524 break;
525 }
526 }
527 let rest = t.strip_prefix("fn ")?;
528 let end = rest
529 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
530 .unwrap_or(rest.len());
531 if end == 0 {
532 return None;
533 }
534 Some(rest[..end].to_string())
535}
536
537/// The innermost [`production_fn_ranges`] entry containing `line_idx`, by name — `None`
538/// if `line_idx` sits outside every production fn (module scope: a `use` declaration,
539/// a `const`/`static`, or a `struct`/`enum` body).
540fn enclosing_fn(line_idx: usize, fn_ranges: &[(String, usize, usize)]) -> Option<String> {
541 fn_ranges
542 .iter()
543 .filter(|(_, start, end)| line_idx >= *start && line_idx <= *end)
544 .min_by_key(|(_, start, end)| end - start)
545 .map(|(name, _, _)| name.clone())
546}
547
548/// The literal text component of [`fs_below_driver`]: some production-scope line names
549/// `std::fs`. Necessary but not sufficient (#1013) — a file can touch `std::fs`
550/// through a glob-imported parent binding without ever spelling it; that resolution
551/// lives in [`production_std_fs_files`], which layers on top of this scan.
552fn has_production_std_fs(src: &str) -> bool {
553 let lines: Vec<&str> = src.lines().collect();
554 let ranges = test_mod_ranges(&lines);
555 for (i, line) in lines.iter().enumerate() {
556 if in_test_range(i, &ranges) {
557 continue;
558 }
559 if line.contains("std::fs") {
560 return true;
561 }
562 }
563 false
564}
565
566/// Indices (into `files`, whose paths are relative to the crate's `src/` root) of the
567/// files that touch `std::fs` in production code — the union of the literal text scan
568/// ([`has_production_std_fs`]) and import resolution: a path whose leading module
569/// segment a production `use` declaration binds to `std::fs` (or an item under it),
570/// either a bare `NAME::` root resolved in the file itself (`use std::{fs, io};` — a
571/// form the substring scan can't see) or in an ancestor module reached through
572/// `use super::*;`, transitively (#1013), or a `super::`/`self::`/`crate::`-qualified
573/// path walked through the module tree to the same bindings (#1016 review — a
574/// descendant may spell `super::fs::read_to_string(p)` with no glob import at all,
575/// one disambiguating edit away from a currently-flagged bare call).
576///
577/// Resolution is Rust-shaped, not hand-tracked (#1013 rejects a special-case list):
578/// a private `use std::fs;` in a parent is visible to descendants because module
579/// privacy is ancestor-scoped, a chain of `use super::*;` globs re-reaches it from
580/// any depth, and a nearer binding of the same name shadows a farther one — whether
581/// that binding is another `use` or a locally-declared type-namespace item (`mod fs;`,
582/// `struct File`, …; value-namespace items like `fn` can't head a `NAME::` path, so
583/// they don't shadow one) — so a child that binds `fs` to something else keeps its
584/// bare `fs::` calls unflagged. Visibility is *not* modelled: a path that names a
585/// too-private binding wouldn't compile anyway, so over-approximating is safe.
586///
587/// Known remaining gaps, accepted as out of reach for a text-level scanner: an
588/// ancestor's `use std::fs::read_to_string;` item import called bare (`read_to_string(p)`)
589/// presents no `::` path segment to resolve — the same import used as a path root
590/// (`File::open`) **is** caught, since item bindings under `std::fs` participate in
591/// the same resolution — and a `use` declaration rustfmt has split across lines is
592/// not parsed. #1013 grepped the three scanned crates for the item-import form, and
593/// the #1016 review for the qualified-path and split-declaration forms — zero hits.
594fn production_std_fs_files(files: &[(PathBuf, String)]) -> Vec<usize> {
595 let facts: Vec<FsImportFacts> = files.iter().map(|(_, src)| fs_import_facts(src)).collect();
596 let parents: Vec<Option<usize>> = files
597 .iter()
598 .map(|(path, _)| module_parent(path, files))
599 .collect();
600 (0..files.len())
601 .filter(|&i| {
602 has_production_std_fs(&files[i].1)
603 || resolves_bare_std_fs(i, &facts, &parents)
604 || resolves_qualified_std_fs(i, &facts, &parents, files)
605 })
606 .collect()
607}
608
609/// Per-file production-scope import facts for [`production_std_fs_files`]'s
610/// resolution. All fields exclude `#[cfg(test)] mod` ranges — a test module's
611/// `use super::*;` or tempdir `fs::write` must not make the file, or its children,
612/// read as production `std::fs` (the `bynk-ide` files' shape).
613#[derive(Default)]
614struct FsImportFacts {
615 /// A production `use super::*;` (optionally `pub`-qualified) — the edge that lets
616 /// this file see its parent module's `use` bindings, and (chained) its ancestors'.
617 glob_imports_super: bool,
618 /// Names production `use` declarations bind to `std::fs` or an item under it:
619 /// `use std::fs;` → `fs`, `use std::fs as x;` → `x`, `use std::{fs, io};` → `fs`,
620 /// `use std::fs::File;` → `File`.
621 std_fs_bindings: BTreeSet<String>,
622 /// Every name a production `use` declaration binds, whatever the target — the
623 /// shadow set: a nearer non-`std::fs` binding of a candidate name stops resolution.
624 use_bound_names: BTreeSet<String>,
625 /// Type-namespace items the file declares (`mod fs;`, `struct File`, `enum`,
626 /// `trait`, `type`, `union`) — these beat a glob-imported name in real Rust, so
627 /// they join [`Self::use_bound_names`] on the shadow side of resolution (#1016
628 /// review). Value-namespace items (`fn`, `const`, `static`) can't head a `NAME::`
629 /// module path and are deliberately not collected.
630 declared_type_names: BTreeSet<String>,
631 /// Identifiers appearing as a bare path root `NAME::` (not preceded by another
632 /// path segment) on a production line — the call-site side of the resolution.
633 bare_path_roots: BTreeSet<String>,
634 /// Segment chains of `super::`/`self::`/`crate::`-qualified paths on production
635 /// lines — `super::fs::read_to_string` records `["super", "fs", "read_to_string"]`.
636 /// These need no glob import to reach an ancestor's binding (#1016 review).
637 qualified_paths: BTreeSet<Vec<String>>,
638}
639
640fn fs_import_facts(src: &str) -> FsImportFacts {
641 let lines: Vec<&str> = src.lines().collect();
642 let ranges = test_mod_ranges(&lines);
643 let mut facts = FsImportFacts::default();
644 for (i, line) in lines.iter().enumerate() {
645 if in_test_range(i, &ranges) {
646 continue;
647 }
648 if let Some(decl) = use_declaration(line) {
649 if decl == "super::*" {
650 facts.glob_imports_super = true;
651 }
652 collect_use_bindings("", decl, &mut facts);
653 }
654 collect_declared_type_name(line, &mut facts.declared_type_names);
655 collect_bare_path_roots(line, &mut facts.bare_path_roots);
656 collect_qualified_paths(line, &mut facts.qualified_paths);
657 }
658 facts
659}
660
661/// The path text of a single-line `use` declaration — `use std::fs;` → `std::fs`,
662/// with an optional `pub`/`pub(crate)`/`pub(in …)` prefix stripped and a trailing
663/// `//` comment tolerated (`use super::*; // parent's fs` must not silently sever
664/// the glob edge for a whole subtree — #1016 review; safe to split on `//` because
665/// a `use` path can contain neither a comment marker nor a string). A declaration
666/// rustfmt has split across lines has no trailing `;` here and is not recognised —
667/// none of the `std::fs` forms in the scanned crates are long enough to split.
668fn use_declaration(line: &str) -> Option<&str> {
669 let mut t = line.trim();
670 if let Some(rest) = t.strip_prefix("pub") {
671 let rest = rest.trim_start();
672 t = if let Some(after_paren) = rest.strip_prefix('(') {
673 after_paren.split_once(')')?.1.trim_start()
674 } else {
675 rest
676 };
677 }
678 let body = t.strip_prefix("use ")?;
679 let body = body.split("//").next().unwrap_or(body);
680 body.trim().strip_suffix(';').map(str::trim)
681}
682
683/// If `line` declares a type-namespace item — `mod`/`struct`/`enum`/`trait`/`type`/
684/// `union`, optionally `pub`-qualified, optionally `unsafe` (traits) — record its
685/// name. Field/variable positions can't start a trimmed line with these keywords, so
686/// a leading-keyword scan is enough for rustfmt-shaped code.
687fn collect_declared_type_name(line: &str, out: &mut BTreeSet<String>) {
688 let mut t = line.trim();
689 if let Some(rest) = t.strip_prefix("pub") {
690 let rest = rest.trim_start();
691 t = if let Some(after_paren) = rest.strip_prefix('(') {
692 match after_paren.split_once(')') {
693 Some((_, after)) => after.trim_start(),
694 None => return,
695 }
696 } else {
697 rest
698 };
699 }
700 if let Some(rest) = t.strip_prefix("unsafe ") {
701 t = rest.trim_start();
702 }
703 for kw in ["mod ", "struct ", "enum ", "trait ", "type ", "union "] {
704 if let Some(rest) = t.strip_prefix(kw) {
705 let rest = rest.trim_start();
706 let end = rest
707 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
708 .unwrap_or(rest.len());
709 if end > 0 {
710 out.insert(rest[..end].to_string());
711 }
712 return;
713 }
714 }
715}
716
717/// Record the name(s) a `use` path binds into `facts` — `path::to::name`,
718/// `path as alias`, and brace groups (`std::{fs, path::PathBuf}`, nested one level
719/// per recursion). `prefix` is the already-consumed leading path (empty at the top).
720fn collect_use_bindings(prefix: &str, entry: &str, facts: &mut FsImportFacts) {
721 let entry = entry.trim();
722 if entry.is_empty() {
723 return;
724 }
725 if let Some((path_part, group)) = entry.split_once('{') {
726 let inner_prefix = join_use_path(prefix, path_part.trim().trim_end_matches("::"));
727 let group = group.strip_suffix('}').unwrap_or(group);
728 for part in split_group_entries(group) {
729 collect_use_bindings(&inner_prefix, part, facts);
730 }
731 return;
732 }
733 let (path_part, alias) = match entry.split_once(" as ") {
734 Some((p, a)) => (p.trim(), Some(a.trim())),
735 None => (entry, None),
736 };
737 let full = join_use_path(prefix, path_part);
738 // `use std::fs::{self};` binds `fs` — normalise the `self` leaf away.
739 let full = full.strip_suffix("::self").unwrap_or(&full);
740 let last = full.rsplit("::").next().unwrap_or(full);
741 let name = alias.unwrap_or(last);
742 if name.is_empty() || name == "*" {
743 return; // globs bind no single name; `super::*` is tracked separately
744 }
745 facts.use_bound_names.insert(name.to_string());
746 if full == "std::fs" || full.starts_with("std::fs::") {
747 facts.std_fs_bindings.insert(name.to_string());
748 }
749}
750
751fn join_use_path(prefix: &str, part: &str) -> String {
752 if prefix.is_empty() {
753 part.to_string()
754 } else {
755 format!("{prefix}::{part}")
756 }
757}
758
759/// Split a brace group's contents on top-level commas only — `fs::{self, File}, io`
760/// is two entries, not three.
761fn split_group_entries(s: &str) -> Vec<&str> {
762 let mut out = Vec::new();
763 let mut depth = 0i32;
764 let mut start = 0;
765 for (i, c) in s.char_indices() {
766 match c {
767 '{' => depth += 1,
768 '}' => depth -= 1,
769 ',' if depth == 0 => {
770 out.push(&s[start..i]);
771 start = i + 1;
772 }
773 _ => {}
774 }
775 }
776 out.push(&s[start..]);
777 out
778}
779
780/// Every identifier `NAME` occurring as `NAME::` where the character before `NAME` is
781/// not `:` — i.e. a path *root*, so `std::fs::read` contributes `std`, never `fs`.
782/// Same line discipline as the text scan: comments included, production scope only
783/// (the caller has already excluded test ranges).
784fn collect_bare_path_roots(line: &str, out: &mut BTreeSet<String>) {
785 let bytes = line.as_bytes();
786 let mut search_from = 0;
787 while let Some(rel) = line[search_from..].find("::") {
788 let pos = search_from + rel;
789 let mut start = pos;
790 while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') {
791 start -= 1;
792 }
793 if start < pos && (start == 0 || bytes[start - 1] != b':') {
794 out.insert(line[start..pos].to_string());
795 }
796 search_from = pos + 2;
797 }
798}
799
800/// Every `super::`/`self::`/`crate::`-rooted path on `line`, as its segment chain —
801/// `super::fs::read_to_string(p)` yields `["super", "fs", "read_to_string"]`. The
802/// root must sit at a bare word boundary (not `a_super::` or `a::super::`), so only
803/// genuine path roots are collected; `Self::` (capital) never matches, and `self.x`
804/// has no `::` to match.
805fn collect_qualified_paths(line: &str, out: &mut BTreeSet<Vec<String>>) {
806 let bytes = line.as_bytes();
807 for root in ["super", "self", "crate"] {
808 let mut from = 0;
809 while let Some(rel) = line[from..].find(root) {
810 let start = from + rel;
811 let root_end = start + root.len();
812 from = root_end;
813 let boundary_ok = start == 0 || {
814 let c = bytes[start - 1];
815 !(c.is_ascii_alphanumeric() || c == b'_' || c == b':')
816 };
817 if !boundary_ok || !line[root_end..].starts_with("::") {
818 continue;
819 }
820 let mut segments = vec![root.to_string()];
821 let mut pos = root_end;
822 while line[pos..].starts_with("::") {
823 let seg_start = pos + 2;
824 let mut seg_end = seg_start;
825 while seg_end < bytes.len()
826 && (bytes[seg_end].is_ascii_alphanumeric() || bytes[seg_end] == b'_')
827 {
828 seg_end += 1;
829 }
830 if seg_end == seg_start {
831 break; // `super::*` and friends — no further identifier
832 }
833 segments.push(line[seg_start..seg_end].to_string());
834 pos = seg_end;
835 }
836 if segments.len() >= 2 {
837 out.insert(segments);
838 }
839 }
840 }
841}
842
843/// The file defining `path`'s parent module, by the standard layout: `a/b.rs`'s parent
844/// is `a.rs` (or `a/mod.rs`), `a/mod.rs`'s parent is the crate root, and the roots
845/// (`lib.rs`/`main.rs`) have none. `#[path]`-remapped modules are not handled — none
846/// exist below the driver, and a text-level probe can't chase them anyway.
847fn module_parent(path: &Path, files: &[(PathBuf, String)]) -> Option<usize> {
848 let stem = path.file_stem()?.to_str()?;
849 let dir = path.parent().filter(|d| !d.as_os_str().is_empty());
850 let parent_module: PathBuf = if stem == "mod" {
851 dir?.parent().map(Path::to_path_buf).unwrap_or_default()
852 } else if let Some(dir) = dir {
853 dir.to_path_buf()
854 } else {
855 if stem == "lib" || stem == "main" {
856 return None;
857 }
858 PathBuf::new()
859 };
860 let candidates = if parent_module.as_os_str().is_empty() {
861 vec![PathBuf::from("lib.rs"), PathBuf::from("main.rs")]
862 } else {
863 vec![
864 parent_module.with_extension("rs"),
865 parent_module.join("mod.rs"),
866 ]
867 };
868 candidates
869 .iter()
870 .find_map(|c| files.iter().position(|(p, _)| p == c))
871}
872
873/// The scopes whose bindings a name used in module `i` can see: the module itself,
874/// then each ancestor reachable while every module below it glob-imports `super::*`.
875fn visible_scopes(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> Vec<usize> {
876 let mut scopes = vec![i];
877 let mut cur = i;
878 loop {
879 if !facts[cur].glob_imports_super {
880 break;
881 }
882 let Some(parent) = parents[cur] else { break };
883 scopes.push(parent);
884 cur = parent;
885 }
886 scopes
887}
888
889/// How `name` resolves in module `m`'s namespace, walking [`visible_scopes`] with
890/// nearest binding winning — a closer non-`std::fs` `use` binding *or* locally
891/// declared type-namespace item shadows a farther `std::fs` binding, as in Rust.
892enum NameResolution {
893 StdFs,
894 Other,
895 Unbound,
896}
897
898fn resolve_name_in_module(
899 m: usize,
900 name: &str,
901 facts: &[FsImportFacts],
902 parents: &[Option<usize>],
903) -> NameResolution {
904 for s in visible_scopes(m, facts, parents) {
905 if facts[s].std_fs_bindings.contains(name) {
906 return NameResolution::StdFs;
907 }
908 if facts[s].use_bound_names.contains(name) || facts[s].declared_type_names.contains(name) {
909 return NameResolution::Other;
910 }
911 }
912 NameResolution::Unbound
913}
914
915/// Does a bare path root in file `i` resolve to `std::fs` through the bindings it
916/// can see? Candidates are the names any visible scope binds to `std::fs`; each is
917/// then resolved from `i` with nearest-binding-wins shadowing.
918fn resolves_bare_std_fs(i: usize, facts: &[FsImportFacts], parents: &[Option<usize>]) -> bool {
919 let scopes = visible_scopes(i, facts, parents);
920 let mut candidates: BTreeSet<&str> = BTreeSet::new();
921 for &s in &scopes {
922 candidates.extend(facts[s].std_fs_bindings.iter().map(String::as_str));
923 }
924 candidates.into_iter().any(|name| {
925 facts[i].bare_path_roots.contains(name)
926 && matches!(
927 resolve_name_in_module(i, name, facts, parents),
928 NameResolution::StdFs
929 )
930 })
931}
932
933/// Does a `super::`/`self::`/`crate::`-qualified path in file `i` reach a `std::fs`
934/// binding (#1016 review)? Unlike the bare-root case these need no glob import: the
935/// root picks the starting module directly (`super`-hops up the parent chain, `self`
936/// the file itself, `crate` the crate root), then each further segment either
937/// resolves in that module's namespace — `std::fs` flags, anything else stops — or
938/// descends into a child module file and continues. Inline `mod name { … }` blocks
939/// are not modelled (their `use` bindings live in the same file, which the text scan
940/// and bare-root resolution already cover).
941fn resolves_qualified_std_fs(
942 i: usize,
943 facts: &[FsImportFacts],
944 parents: &[Option<usize>],
945 files: &[(PathBuf, String)],
946) -> bool {
947 facts[i]
948 .qualified_paths
949 .iter()
950 .any(|chain| qualified_chain_reaches_std_fs(chain, i, facts, parents, files))
951}
952
953fn qualified_chain_reaches_std_fs(
954 chain: &[String],
955 i: usize,
956 facts: &[FsImportFacts],
957 parents: &[Option<usize>],
958 files: &[(PathBuf, String)],
959) -> bool {
960 let mut idx = 1;
961 let mut m = match chain[0].as_str() {
962 "self" => i,
963 "crate" => {
964 let root = files
965 .iter()
966 .position(|(p, _)| p == Path::new("lib.rs") || p == Path::new("main.rs"));
967 match root {
968 Some(root) => root,
969 None => return false,
970 }
971 }
972 "super" => {
973 let mut m = i;
974 idx = 0;
975 while idx < chain.len() && chain[idx] == "super" {
976 let Some(parent) = parents[m] else {
977 return false;
978 };
979 m = parent;
980 idx += 1;
981 }
982 m
983 }
984 _ => return false,
985 };
986 while idx < chain.len() {
987 let seg = chain[idx].as_str();
988 // Resolve `seg` in `m`, nearest scope first. Within a scope, a child module
989 // file for `seg` is checked *before* the shadow set: a declared `mod seg;`
990 // lands `seg` in `declared_type_names`, but that declaration IS the child
991 // module — it's the path's next hop, not a shadow over it. (In valid Rust a
992 // module and another same-name type-namespace item can't coexist in one
993 // scope, so the ordering costs nothing.)
994 let mut next = None;
995 for s in visible_scopes(m, facts, parents) {
996 if facts[s].std_fs_bindings.contains(seg) {
997 return true;
998 }
999 if let Some(child) = child_module_file(s, seg, files) {
1000 next = Some(child);
1001 break;
1002 }
1003 if facts[s].use_bound_names.contains(seg) || facts[s].declared_type_names.contains(seg)
1004 {
1005 return false; // bound to something that is neither std::fs nor a module
1006 }
1007 }
1008 let Some(child) = next else {
1009 return false;
1010 };
1011 m = child;
1012 idx += 1;
1013 }
1014 false
1015}
1016
1017/// The file defining module `m`'s child module `seg`, if it exists as a file:
1018/// `lib.rs` + `a` → `a.rs`/`a/mod.rs`, `a.rs` + `b` → `a/b.rs`/`a/b/mod.rs`,
1019/// `a/mod.rs` + `b` → `a/b.rs`/`a/b/mod.rs`.
1020fn child_module_file(m: usize, seg: &str, files: &[(PathBuf, String)]) -> Option<usize> {
1021 let m_path = &files[m].0;
1022 let module_dir: PathBuf = match m_path.file_stem().and_then(|s| s.to_str()) {
1023 Some("mod") => m_path.parent().unwrap_or(Path::new("")).to_path_buf(),
1024 Some("lib") | Some("main") if m_path.parent().is_none_or(|p| p.as_os_str().is_empty()) => {
1025 PathBuf::new()
1026 }
1027 _ => m_path.with_extension(""),
1028 };
1029 let candidates = [
1030 module_dir.join(format!("{seg}.rs")),
1031 module_dir.join(seg).join("mod.rs"),
1032 ];
1033 candidates
1034 .iter()
1035 .find_map(|c| files.iter().position(|(p, _)| p == c))
1036}
1037
1038/// Every `#[cfg(test)] mod <ident> { ... }` block in `lines`, as inclusive
1039/// `(start_line, end_line)` line-index ranges — every occurrence, not just a single
1040/// trailing block. A file in this codebase can carry several test modules scattered
1041/// through it with production code between them — `bynk-emit/src/emitter/lower.rs` has
1042/// two, 1031 lines apart, and treating "everything after the first (or last)
1043/// `#[cfg(test)]`" as one cutoff silently misclassifies that intervening production
1044/// code as test-scope (caught in review: it made `fs_below_driver`, a *gated* probe,
1045/// blind over that span, and inflated `test_density`'s ratio by up to 39%).
1046///
1047/// A block's end is found by real brace-depth counting via [`brace_delta`], not a
1048/// "first column-0 `}`" shortcut: an earlier version of this fix tried exactly that
1049/// shortcut (reasoning that rustfmt always dedents a closing brace back to column 0)
1050/// and it broke on files like `bynk-ide/src/sequence.rs`, whose test module embeds
1051/// multi-line `.bynk`/TypeScript fixture source as string literals — source that
1052/// itself contains a column-0 `}` closing a top-level construct *inside the string*,
1053/// which the shortcut mistook for the end of the Rust `mod` block, truncating it by
1054/// hundreds of lines. `brace_delta` skips characters inside Rust string/char literals
1055/// and comments, so embedded fixture text can't be mistaken for real Rust braces.
1056///
1057/// Only matches a brace-opening `mod` line — `#[cfg(test)] mod foo;` (an external-file
1058/// declaration, not an inline scope) does not open a range.
1059fn test_mod_ranges(lines: &[&str]) -> Vec<(usize, usize)> {
1060 let mut ranges = Vec::new();
1061 let mut i = 0;
1062 while i < lines.len() {
1063 if lines[i].trim() == "#[cfg(test)]"
1064 && let Some(off) = lines[i + 1..].iter().position(|l| !l.trim().is_empty())
1065 {
1066 let mod_line = i + 1 + off;
1067 let t = lines[mod_line].trim();
1068 if t.starts_with("mod ") && t.ends_with('{') {
1069 let mut depth = 0i32;
1070 let mut state = BraceScanState::Normal;
1071 let mut started = false;
1072 let mut end = lines.len() - 1;
1073 for (j, line) in lines[mod_line..].iter().enumerate() {
1074 let (delta, new_state) = brace_delta(line, state);
1075 state = new_state;
1076 depth += delta;
1077 if depth != 0 {
1078 started = true;
1079 }
1080 if started && depth == 0 {
1081 end = mod_line + j;
1082 break;
1083 }
1084 }
1085 ranges.push((mod_line, end));
1086 i = end + 1;
1087 continue;
1088 }
1089 }
1090 i += 1;
1091 }
1092 ranges
1093}
1094
1095fn in_test_range(line_idx: usize, ranges: &[(usize, usize)]) -> bool {
1096 ranges
1097 .iter()
1098 .any(|(start, end)| line_idx >= *start && line_idx <= *end)
1099}
1100
1101/// Scanner state carried across lines for [`brace_delta`]: whether the cursor is
1102/// inside a string literal, a raw string (with its `#`-count), or a block comment
1103/// (with nesting depth — Rust block comments nest).
1104#[derive(Clone, Copy, PartialEq)]
1105enum BraceScanState {
1106 Normal,
1107 InString,
1108 InRawString(u8),
1109 InBlockComment(u32),
1110}
1111
1112/// The net `{`/`}` depth change in `line`, skipping characters inside Rust string/char
1113/// literals, raw strings, and line/block comments — a naive per-character brace count
1114/// breaks the moment a line contains a fixture string like `"fn f() { \"{\" }"` or a
1115/// doc comment mentioning a brace. Returns the depth delta and the state to carry into
1116/// the next line (a string or block comment can span line boundaries).
1117fn brace_delta(line: &str, mut state: BraceScanState) -> (i32, BraceScanState) {
1118 let mut delta = 0i32;
1119 let chars: Vec<char> = line.chars().collect();
1120 let mut i = 0;
1121 while i < chars.len() {
1122 match state {
1123 BraceScanState::Normal => {
1124 if chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
1125 break; // rest of the line is a line comment
1126 }
1127 if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1128 state = BraceScanState::InBlockComment(1);
1129 i += 2;
1130 continue;
1131 }
1132 if chars[i] == '"' {
1133 state = BraceScanState::InString;
1134 i += 1;
1135 continue;
1136 }
1137 if chars[i] == 'r' && matches!(chars.get(i + 1), Some('"') | Some('#')) {
1138 let mut j = i + 1;
1139 let mut hashes = 0u8;
1140 while chars.get(j) == Some(&'#') {
1141 hashes += 1;
1142 j += 1;
1143 }
1144 if chars.get(j) == Some(&'"') {
1145 state = BraceScanState::InRawString(hashes);
1146 i = j + 1;
1147 continue;
1148 }
1149 }
1150 if chars[i] == '\'' {
1151 // A `'\x'`/`'\\'`-style escaped char literal, or a plain `'x'` —
1152 // skip past it so its contents can't be mistaken for braces.
1153 // Anything else (no closing `'` within a couple of chars) is a
1154 // lifetime, which owns no closing quote to skip.
1155 if chars.get(i + 1) == Some(&'\\') {
1156 let mut j = i + 2;
1157 while j < chars.len() && chars[j] != '\'' {
1158 j += 1;
1159 }
1160 i = (j + 1).min(chars.len());
1161 continue;
1162 } else if chars.get(i + 2) == Some(&'\'') {
1163 i += 3;
1164 continue;
1165 }
1166 }
1167 match chars[i] {
1168 '{' => delta += 1,
1169 '}' => delta -= 1,
1170 _ => {}
1171 }
1172 i += 1;
1173 }
1174 BraceScanState::InString => {
1175 if chars[i] == '\\' {
1176 i += 2;
1177 continue;
1178 }
1179 if chars[i] == '"' {
1180 state = BraceScanState::Normal;
1181 }
1182 i += 1;
1183 }
1184 BraceScanState::InRawString(hashes) => {
1185 if chars[i] == '"' {
1186 let mut j = i + 1;
1187 let mut h = 0u8;
1188 while chars.get(j) == Some(&'#') && h < hashes {
1189 h += 1;
1190 j += 1;
1191 }
1192 if h == hashes {
1193 state = BraceScanState::Normal;
1194 i = j;
1195 continue;
1196 }
1197 }
1198 i += 1;
1199 }
1200 BraceScanState::InBlockComment(depth) => {
1201 if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
1202 state = BraceScanState::InBlockComment(depth + 1);
1203 i += 2;
1204 continue;
1205 }
1206 if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
1207 state = if depth <= 1 {
1208 BraceScanState::Normal
1209 } else {
1210 BraceScanState::InBlockComment(depth - 1)
1211 };
1212 i += 2;
1213 continue;
1214 }
1215 i += 1;
1216 }
1217 }
1218 }
1219 (delta, state)
1220}
1221
1222// --- Gated probe 3: options_sources --------------------------------------
1223
1224/// R2.3. `CompileOptions` (in `bynk-emit/src/project.rs`) has a `sources` field.
1225fn options_sources(root: &Path) -> Probe {
1226 let src = std::fs::read_to_string(root.join("bynk-emit/src/project.rs")).unwrap_or_default();
1227 let present = struct_body(&src, "CompileOptions").is_some_and(|body| body.contains("sources"));
1228 Probe {
1229 name: "options_sources",
1230 gated: true,
1231 reads: if present {
1232 "present".to_string()
1233 } else {
1234 "absent".to_string()
1235 },
1236 }
1237}
1238
1239/// The `{ ... }` body text of `struct <name>` in `src`, brace-matched from the struct's
1240/// own opening brace to its close.
1241fn struct_body<'a>(src: &'a str, name: &str) -> Option<&'a str> {
1242 let needle = format!("struct {name}");
1243 let start = src.find(&needle)?;
1244 let open = start + src[start..].find('{')?;
1245 let mut depth = 0i32;
1246 for (offset, ch) in src[open..].char_indices() {
1247 match ch {
1248 '{' => depth += 1,
1249 '}' => {
1250 depth -= 1;
1251 if depth == 0 {
1252 return Some(&src[open..open + offset + 1]);
1253 }
1254 }
1255 _ => {}
1256 }
1257 }
1258 None
1259}
1260
1261// --- Gated probe 4: hoist_sinks -------------------------------------------
1262
1263/// R6.2. Live (non-comment) occurrences of the sink-passing signature
1264/// `stmts: &mut Vec<String>` in `bynk-emit`. Tier B (T2.1) deletes it entirely.
1265fn hoist_sinks(root: &Path) -> Probe {
1266 let dir = root.join("bynk-emit/src");
1267 let needle = "stmts: &mut Vec<String>";
1268 let mut count = 0usize;
1269 for (_, contents) in rust_files(&dir) {
1270 for line in contents.lines() {
1271 if !is_line_comment(line) && line.contains(needle) {
1272 count += 1;
1273 }
1274 }
1275 }
1276 Probe {
1277 name: "hoist_sinks",
1278 gated: true,
1279 reads: count.to_string(),
1280 }
1281}
1282
1283// --- Gated probe 5: span_keyed_maps ---------------------------------------
1284
1285/// R2.4. Whole-repo occurrences of `HashMap<Span` (comments included — the phase-3
1286/// migration target is every mention, not just live call sites), **excluding
1287/// `xtask` itself**: this probe's own doc comment and source both name the search
1288/// string, which would otherwise self-count every time this file is touched — the
1289/// same self-reference hazard flagged for the dead-identifier probes below, caught
1290/// here by running the probe against itself before committing the first table.
1291fn span_keyed_maps(root: &Path) -> Probe {
1292 let count = count_repo_wide(root, "HashMap<Span", &["xtask"]);
1293 Probe {
1294 name: "span_keyed_maps",
1295 gated: true,
1296 reads: count.to_string(),
1297 }
1298}
1299
1300fn count_repo_wide(root: &Path, needle: &str, exclude_crates: &[&str]) -> usize {
1301 let mut total = 0usize;
1302 for entry in top_level_crate_dirs(root) {
1303 if exclude_crates
1304 .iter()
1305 .any(|c| entry.file_name().is_some_and(|n| n == *c))
1306 {
1307 continue;
1308 }
1309 for (_, contents) in rust_files(&entry.join("src")) {
1310 total += contents.matches(needle).count();
1311 }
1312 }
1313 total
1314}
1315
1316/// Every workspace member crate directory (anything at the repo root with a
1317/// `Cargo.toml` and a `src/` dir), excluding `target` and non-crate directories.
1318fn top_level_crate_dirs(root: &Path) -> Vec<PathBuf> {
1319 let mut out = Vec::new();
1320 let Ok(entries) = std::fs::read_dir(root) else {
1321 return out;
1322 };
1323 for entry in entries.flatten() {
1324 let path = entry.path();
1325 if path.is_dir() && path.join("Cargo.toml").is_file() && path.join("src").is_dir() {
1326 out.push(path);
1327 }
1328 }
1329 out.sort();
1330 out
1331}
1332
1333// --- Gated probe 6: emit_diagnostics --------------------------------------
1334
1335/// R3.5. `bynk.*` string literals in `bynk-emit`/`bynk-check` source, cross-referenced
1336/// against `bynk_syntax::diagnostics::REGISTRY` — not pattern-matched. A literal not in
1337/// `REGISTRY` is a commons/namespace path (e.g. `bynk.locale`, the compiled first-party
1338/// source module name), not a diagnostic code, and must not inflate the count (#999
1339/// Decision A: this cross-reference is what makes the exclusion correct by
1340/// construction rather than a second hand-maintained list).
1341fn emit_diagnostics(root: &Path) -> Probe {
1342 let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
1343 .iter()
1344 .map(|d| d.code)
1345 .collect();
1346 let mut parts = Vec::new();
1347 for (label, dir) in [
1348 ("bynk-emit", "bynk-emit/src"),
1349 ("bynk-check", "bynk-check/src"),
1350 ] {
1351 let mut naive: BTreeSet<String> = BTreeSet::new();
1352 for (_, contents) in rust_files(&root.join(dir)) {
1353 for lit in bynk_dotted_literals(&contents) {
1354 naive.insert(lit.to_string());
1355 }
1356 }
1357 let true_count = naive
1358 .iter()
1359 .filter(|l| registry.contains(l.as_str()))
1360 .count();
1361 parts.push(format!("{label}={true_count}/{}", naive.len()));
1362 }
1363 Probe {
1364 name: "emit_diagnostics",
1365 gated: true,
1366 reads: format!("{} (true/naive)", parts.join(", ")),
1367 }
1368}
1369
1370// --- Gated probe 7: ide_emit_edge -----------------------------------------
1371
1372/// R10.2. `bynk-ide` → `bynk-emit` in the manifest (`bynk-emit.workspace = true` or an
1373/// equivalent path/version dependency line).
1374fn ide_emit_edge(root: &Path) -> Probe {
1375 let manifest = std::fs::read_to_string(root.join("bynk-ide/Cargo.toml")).unwrap_or_default();
1376 let present = manifest
1377 .lines()
1378 .any(|l| l.trim_start().starts_with("bynk-emit"));
1379 Probe {
1380 name: "ide_emit_edge",
1381 gated: true,
1382 reads: if present {
1383 "present".to_string()
1384 } else {
1385 "absent".to_string()
1386 },
1387 }
1388}
1389
1390// --- Gated probe 8: ast_importers -----------------------------------------
1391
1392/// #1176: `bynk-emit::ir`'s own two files — named exactly, not by path prefix, the same
1393/// permanent-carve-out discipline [`NAMED_FS_EXCEPTIONS`] and [`emit_diagnostics`]'s
1394/// registry cross-reference already use. An `Ast → Ir` lowering pass importing
1395/// `bynk_syntax::ast` is that pass's entire job, not the AST-walking this track is
1396/// closing (phase 6's own P6.9 correction, #1167 — see the retired `the-ir.md`'s
1397/// closing summary, `design/archive/retired-tracks.md`) — but `project.rs` also
1398/// imports `bynk_syntax::ast` today (`EmitProjectCtx` holding `ActorDecl`/`AgentDecl`
1399/// fields directly), and that *is* exactly the still-open R6.13 defect this probe
1400/// tracks (P6.6: "closes the emitter reading AST declarations directly"). A
1401/// path-prefix rule scoped to `emitter/**` would exclude that file right along with
1402/// `ir/`'s legitimate ones, silently undercounting real remaining work — see
1403/// [`is_named_ast_importer`].
1404///
1405/// #1184 review: this exclusion is necessary but not sufficient for R6.13. `bynk-ir`
1406/// still embeds AST types directly in IR struct fields (`TypeShape::Refined`'s
1407/// `BaseType`/`Refinement`, ADR 0366) rather than IR-native equivalents — an emitter
1408/// reading such a field touches the AST without ever spelling `bynk_syntax::ast`
1409/// itself, so it is invisible to this probe by construction. `ast_importers` reading
1410/// its retired floor (5 — `design/archive/retired-tracks.md`'s own closing summary
1411/// has the per-file argument) proves no *remaining* file outside these two and the
1412/// five-file rendering subtree imports the AST module directly; it does not by
1413/// itself prove every `bynk-ir` field is AST-free.
1414///
1415/// #1187's own closing scoping pass adds one more, on different grounds than the
1416/// `ir.rs`/`ir/lower.rs` pair above: `project/tests_emit.rs` was deliberately *not*
1417/// added alongside `project.rs` when this list was first cut (the
1418/// `ast_importer_exclusion_is_named_not_prefixed` test below used to assert exactly
1419/// that) — #1187's own scoping pass found new evidence changing that: its test/suite
1420/// case bodies call `emitter::lower_block_to_async_body`/`lower_test_case_body`/
1421/// `lower_integration_case_body` directly (the Q7-settled body-rendering pass —
1422/// `emitter/lower.rs` keeps hand-writing TypeScript source text after phase 6's
1423/// cutover, the printer that would change that is phase 7's), and
1424/// its own `driver_param_ty`/`strip_effect_httpresult` read a handler's *declared*
1425/// param/return `TypeRef` with no corresponding `TyId` available at that call site
1426/// (the same caller-reads-callee's-raw-declared-shape pattern #661 established for
1427/// cross-context codec generation). Both are the Q7/printer kind of unreachable, not
1428/// the "still open, real work" kind the original exclusion list deliberately left this
1429/// file out of — the correction is new evidence, not a reversal of that reasoning.
1430///
1431/// Review of #1210: `emitter.rs`/`emitter/lower.rs` themselves were considered for
1432/// this same exclusion and **rejected** — Q7 settles that these files' *body-rendering*
1433/// surface stays AST-parameter-driven, but both files also hold live, currently
1434/// untouched AST-*declaration* reads with no such gate: `emitter.rs`'s own
1435/// `CommonsItem::Service`/`svc.protocol` walk (consumed-event-root collection) and
1436/// `emitter/lower.rs`'s own `cap_op_param_names` (`CommonsItem::Capability`/`c.ops`)
1437/// were exactly the P6.2/P6.6-class conversions phase 6's own slice decomposition
1438/// still listed as in scope at the time, not body-rendering. Excluding either file
1439/// would have hidden that real, fixable surface from this probe the same way a
1440/// path-prefix rule would — the harm the named-not-prefixed discipline above exists
1441/// to prevent, just at file granularity instead of directory granularity. (Both
1442/// converted their own reachable surface later, without joining this list — phase 6's
1443/// closing summary, `design/archive/retired-tracks.md`, has the account.)
1444///
1445/// P6.33 (phase 6's own §6a.D re-settling, 19 August 2026): `emitter/serialisation.rs`
1446/// joins the list, on grounds distinct from every entry above — not Q7 body-rendering,
1447/// not test-only reach, but a phase boundary. Unlike `emitter.rs`/`emitter/lower.rs`,
1448/// this file holds no `CommonsItem`-declaration-read surface at all (confirmed:
1449/// `grep -c bynk_syntax::ast` finds only its one `use` line and its `#[cfg(test)]`
1450/// module) — its entire AST surface, ~120 sites, *is* the `TypeRef`-driven JSON/wire
1451/// codec renderer (`emit_record_codec`/`emit_sum_codec`/`serialise_expr`/
1452/// `deserialise_expr`/`ts_inner_type` and siblings). Rendering a checker type as TS
1453/// codec source text is the same class of question Q7 already settled belongs to the
1454/// eventual printer (phase 7, `bynk-ts`) — this file has no `use crate::ir` at all, so
1455/// nothing here has been resisting an available IR-native alternative; none exists. The
1456/// re-settling found no clean way to shrink this file's AST surface further without
1457/// building printer infrastructure phase 6's own scope already excluded.
1458///
1459/// P6.49 (phase 6's own §6b, 19 August 2026): `project.rs` — R6.13's own still-open
1460/// declaration-read surface named at the top of this doc block, above — cleared
1461/// **without joining this list**. Nine slices (P6.42–P6.49) relocated its remaining
1462/// declaration reads to the `bynk-check`/`bynk-project` crates that already own the
1463/// data (`SourceUnit::name()`, `own_contract_hashes`, `discover_event_subscribers`,
1464/// `combined_types_for_unit_info`, two owner-side accessors,
1465/// `lower_event_subscriber_shapes_ir`, `walk_unit_table_bodies`) or re-exported a type
1466/// from a `bynk-check` module whose own public API was already parameterised by it
1467/// (`TypeDecl`/`FnDecl`/`Visibility` from `project_model`, `ActorDecl` from `actors` —
1468/// the P6.27 `ExprId` precedent, applied four more times). This is the evidence this
1469/// exclusion list's own entries above are real, earned exclusions and not a standing
1470/// habit: the harder file cleared its own way, on its own schedule, with zero new
1471/// entries here.
1472///
1473/// **P6.58/P6.59, 19 August 2026: phase 6 (`the-ir.md`, spine #1137) retired at this
1474/// probe reading 5, not 0.** The floor is exactly `bynk-emit/src/emitter{,/**}` —
1475/// `emitter.rs`, `emitter/emit.rs`, `emitter/lower.rs`, `emitter/workers.rs`,
1476/// `emitter/workers_entry.rs` — the TypeScript-rendering subtree phase 7's own printer
1477/// inherits; each file's own structural reason, and the full slice history behind
1478/// every correction this doc block narrates, live in `design/archive/retired-tracks.md`
1479/// now that `the-ir.md` itself is gone. This exclusion list does **not** grow to
1480/// reach that floor — the floor is a fact about `AST_IMPORTER_EXCEPTIONS`'s own
1481/// four entries staying exactly these four, not a fifth argument for adding to them.
1482/// The probe itself stays gated, unchanged, reading 5: a regression ratchet phase 7
1483/// inherits and drives down as it builds the printer this floor's own residue names.
1484///
1485/// **Arc D, P7.12 (crate carve): `ir.rs`/`ir/lower.rs` drop out of this list
1486/// entirely — not because they stopped importing the AST (unchanged, still
1487/// do), but because they left `bynk-emit/src` altogether, carved into the new
1488/// `bynk-ir`/`bynk-lower` crates ADR 0332 deferred and ADR 0385 triggered.**
1489/// This probe was never scoped to those crates (`ast_importer_files` walks
1490/// `bynk-emit/src` only), so the pair is simply outside its universe now,
1491/// the same way a file moving to `bynk-check`/`bynk-project` already leaves
1492/// silently rather than needing its own exclusion-list removal step. Two
1493/// named exclusions remain.
1494const AST_IMPORTER_EXCEPTIONS: &[&str] = &["project/tests_emit.rs", "emitter/serialisation.rs"];
1495
1496/// Is `rel_path` (relative to `bynk-emit/src`) one of [`AST_IMPORTER_EXCEPTIONS`]?
1497fn is_named_ast_importer(rel_path: &Path) -> bool {
1498 let rel = rel_path.to_string_lossy().replace('\\', "/");
1499 AST_IMPORTER_EXCEPTIONS.contains(&rel.as_str())
1500}
1501
1502/// Is `contents` a module (not a nested block) that glob-imports its parent —
1503/// i.e. does it carry a top-level (column-0) `use super::*;`? Rust's own privacy
1504/// rule makes a parent module's private `use` visible to descendant modules, so a
1505/// file matching this can expose `bynk_syntax::ast` names it never spells itself
1506/// (P6.26 review, #1259) — deliberately column-0 only, so a `use super::*;`
1507/// *inside* a nested `#[cfg(test)] mod tests { .. }` block (glob-importing its own
1508/// immediately-enclosing module, not the grandparent file on disk) doesn't
1509/// false-positive this check.
1510fn has_module_level_super_glob(contents: &str) -> bool {
1511 contents.lines().any(|line| line == "use super::*;")
1512}
1513
1514/// For `rel_path` = `<dir>/<file>.rs`, does the sibling module file `<dir>.rs`
1515/// (the parent module a top-level `use super::*;` in `rel_path` would inherit
1516/// from) itself contain `bynk_syntax::ast`? `None` if `rel_path` has no such
1517/// parent (a file directly under `bynk-emit/src`, e.g. `emitter.rs` itself).
1518fn super_glob_parent_imports_ast(dir: &Path, rel_path: &Path) -> Option<bool> {
1519 let parent_dir = rel_path.parent()?;
1520 if parent_dir.as_os_str().is_empty() {
1521 return None;
1522 }
1523 let parent_file = dir.join(parent_dir).with_extension("rs");
1524 Some(
1525 std::fs::read_to_string(&parent_file)
1526 .is_ok_and(|contents| contents.contains("bynk_syntax::ast")),
1527 )
1528}
1529
1530/// The files [`ast_importers`] counts: `bynk-emit/src` files whose contents match
1531/// `bynk_syntax::ast` **or** that inherit it from an AST-importing parent through a
1532/// top-level `use super::*;` (P6.26 review, #1259 — a file that stops spelling the
1533/// AST module directly by deleting its own explicit import, while a live `use
1534/// super::*;` still channels a still-AST-importing parent's names in, must stay
1535/// counted; otherwise a future partial conversion could silently drop this probe
1536/// without the underlying AST dependency actually being gone), excluding
1537/// [`AST_IMPORTER_EXCEPTIONS`]. Split out from [`ast_importers`] so a test can
1538/// assert on the actual survivor set, not just its length (#1184 review).
1539fn ast_importer_files(root: &Path) -> Vec<PathBuf> {
1540 let dir = root.join("bynk-emit/src");
1541 rust_files(&dir)
1542 .into_iter()
1543 .filter(|(path, contents)| {
1544 contents.contains("bynk_syntax::ast")
1545 || (has_module_level_super_glob(contents)
1546 && super_glob_parent_imports_ast(&dir, path.strip_prefix(&dir).unwrap_or(path))
1547 .unwrap_or(false))
1548 })
1549 .filter(|(path, _)| !is_named_ast_importer(path.strip_prefix(&dir).unwrap_or(path)))
1550 .map(|(path, _)| path)
1551 .collect()
1552}
1553
1554/// R6.13. Files in `bynk-emit/src` that import `bynk_syntax::ast`, excluding
1555/// [`AST_IMPORTER_EXCEPTIONS`] — phase 6's own remaining AST import surface (retired,
1556/// spine #1137; `design/archive/retired-tracks.md` has the closing summary). #1176:
1557/// the unexcluded, crate-wide count could never reach 0 while `bynk-emit::ir`'s
1558/// lowering pass exists at all; this exclusion is what let the probe track phase 6's
1559/// real completion criterion instead of a floor its own IR module structurally could
1560/// not clear. Gated at 5, phase 6's own retired floor, for phase 7 to drive down.
1561fn ast_importers(root: &Path) -> Probe {
1562 Probe {
1563 name: "ast_importers",
1564 gated: true,
1565 reads: ast_importer_files(root).len().to_string(),
1566 }
1567}
1568
1569// --- Gated probe 9: emit_abi_shapes ---------------------------------------
1570
1571/// ADR 0310 D1's four emit-ABI shapes, as they surface as import names in the vendored
1572/// bindings — the `Result`/`Option` tag layout plus `JsonError`, `Uuid`, `FetchError`.
1573const EMIT_ABI: &[&str] = &[
1574 "Result",
1575 "Option",
1576 "Ok",
1577 "Err",
1578 "Some",
1579 "None",
1580 "JsonError",
1581 "Uuid",
1582 "FetchError",
1583];
1584
1585/// The capability interfaces a vendored binding legitimately imports to implement what
1586/// it declares — governed by language-stability rules, not ADR 0310's codegen-freeze
1587/// concern. See [`emit_abi_shapes`] and #999 Decision E for the two-list rationale.
1588const CAPABILITY_SURFACE: &[&str] = &[
1589 "Clock",
1590 "Fetch",
1591 "Idempotency",
1592 "Locale",
1593 "Logger",
1594 "Random",
1595 "Secrets",
1596 "Request",
1597 "Response",
1598 "LocaleTag",
1599 "Kv",
1600 "KVNamespace",
1601 // Interop I5 (#1972): `bynk.cloudflare`'s `Service`, and the Cloudflare
1602 // `Fetch` provider its binding sends through, so the two share one set of
1603 // header and response rules. Binding-to-binding, not a codegen shape.
1604 "Service",
1605 "FetchProvider",
1606];
1607
1608/// Is `ident` one of ADR 0310's enumerated emit-ABI shapes, or part of the capability
1609/// surface a binding is required to import? If neither, it's a leak `emit_abi_shapes`
1610/// flags — this is the single predicate both the probe and its tests use, so a test
1611/// asserting "no leak" can't silently pass against a list the test itself redefined.
1612fn is_enumerated_emit_abi_or_capability_surface(ident: &str) -> bool {
1613 EMIT_ABI.contains(&ident) || CAPABILITY_SURFACE.contains(&ident)
1614}
1615
1616/// The directories holding the vendored first-party TypeScript bindings, relative to
1617/// the repo root (standard-library track, #1940): the `bynk` surface's per-platform
1618/// bindings in `bynk-std`'s portable layer, and the Cloudflare platform adapter's
1619/// binding in its own subtree. [`emit_abi_shapes`] reads every `.ts` file in each,
1620/// non-recursively, so a binding added to a new subtree must be listed here.
1621const EMIT_ABI_BINDING_DIRS: &[&str] = &["bynk-std/lib/core/bindings", "bynk-std/lib/cloudflare"];
1622
1623/// ADR 0310's probe (#999 Decision E). The vendored first-party bindings under
1624/// [`EMIT_ABI_BINDING_DIRS`] must reference only [`EMIT_ABI`]'s nine names.
1625///
1626/// This does NOT count every non-enumerated import: a binding legitimately imports the
1627/// [`CAPABILITY_SURFACE`] interfaces it implements — that surface is governed by
1628/// language-stability rules, not ADR 0310's codegen-freeze concern, and a probe that
1629/// flagged it would read non-zero on every binding by construction. See #999 Decision
1630/// E for the two-list rationale and its falsifier.
1631///
1632/// A listed directory that is missing, or holds no `.ts` file, reads as a failure
1633/// rather than as zero leaks: a binding moved out of the probe's sight must not pass
1634/// it vacuously (#1940 Decision D).
1635fn emit_abi_shapes(root: &Path) -> Probe {
1636 let mut files: Vec<std::path::PathBuf> = Vec::new();
1637 for rel in EMIT_ABI_BINDING_DIRS {
1638 let Ok(entries) = std::fs::read_dir(root.join(rel)) else {
1639 return Probe {
1640 name: "emit_abi_shapes",
1641 gated: true,
1642 reads: format!("bindings directory not found: {rel}"),
1643 };
1644 };
1645 let before = files.len();
1646 files.extend(
1647 entries
1648 .flatten()
1649 .map(|e| e.path())
1650 .filter(|path| path.extension().is_some_and(|e| e == "ts")),
1651 );
1652 if files.len() == before {
1653 return Probe {
1654 name: "emit_abi_shapes",
1655 gated: true,
1656 reads: format!("no bindings found in {rel}"),
1657 };
1658 }
1659 }
1660 // By file name, so the reading does not depend on which directory a binding sits in.
1661 files.sort_by_key(|path| path.file_name().map(|n| n.to_os_string()));
1662 let mut leaks: Vec<String> = Vec::new();
1663 for path in files {
1664 let Ok(contents) = std::fs::read_to_string(&path) else {
1665 continue;
1666 };
1667 let name = path.file_name().unwrap().to_string_lossy().to_string();
1668 for ident in ts_named_imports_from_runtime_modules(&contents) {
1669 if !is_enumerated_emit_abi_or_capability_surface(&ident) {
1670 leaks.push(format!("{name}:{ident}"));
1671 }
1672 }
1673 }
1674 Probe {
1675 name: "emit_abi_shapes",
1676 gated: true,
1677 reads: format!("{} ({})", leaks.len(), leaks.join(", ")),
1678 }
1679}
1680
1681// --- Gated probe 10: ts_writes ---------------------------------------------
1682
1683/// Files under `bynk-emit/src` that contain `write!`/`writeln!`/`format!` calls but
1684/// produce no TypeScript at all — excluded from both [`ts_writes`] and [`ts_any`], each
1685/// argued individually the same way [`AST_IMPORTER_EXCEPTIONS`] is, not assumed from a
1686/// path prefix: `emitter/wrangler.rs` writes `wrangler.toml`; `emitter/secrets.rs`
1687/// writes `bynk-secrets.json`; `emitter/contracts.rs` writes `bynk-contracts.json`;
1688/// `testkit.rs` builds a `.bynk` source fixture — a compiler *input* for tests, not
1689/// output. P7.3 (#1303): `emitter/toml_doc.rs` writes `wrangler.toml` text too —
1690/// `emitter/wrangler.rs`'s own writes moved here when it stopped building the TOML text
1691/// directly and started building a typed `TomlDocument` for this module to print — same
1692/// rationale, same exclusion.
1693///
1694/// (`ir/lower.rs` — Rust-internal `String` values stored on `Ir*` struct fields during
1695/// the checker→IR lowering pass, never emitted syntax — was excluded here for the same
1696/// reason until Arc D's P7.12 crate carve moved it to `bynk-lower` entirely, outside
1697/// this probe's own `bynk-emit/src` universe; no exclusion needed for a file this probe
1698/// no longer walks. `emitter/source_map.rs`, which wrote source-map JSON, is the same
1699/// shape one carve earlier — P7.5 (#1308) relocated it to `bynk-ts/src/source_map.rs`
1700/// in full; its own #1561 removal from this list.)
1701const TS_WRITES_EXCLUDED_FILES: &[&str] = &[
1702 "emitter/wrangler.rs",
1703 "emitter/toml_doc.rs",
1704 "emitter/secrets.rs",
1705 "emitter/contracts.rs",
1706 "testkit.rs",
1707];
1708
1709/// Is `rel_path` (relative to `bynk-emit/src`) one of [`TS_WRITES_EXCLUDED_FILES`]?
1710fn is_ts_writes_excluded_file(rel_path: &Path) -> bool {
1711 let rel = rel_path.to_string_lossy().replace('\\', "/");
1712 TS_WRITES_EXCLUDED_FILES.contains(&rel.as_str())
1713}
1714
1715/// True if `line` builds a filesystem path via `format!` rather than TypeScript text —
1716/// the `PathBuf::from(format!(...))`/`.join(format!(...))`/`.with_file_name(format!(...))`
1717/// idiom [`ts_writes`] excludes at line granularity, not by file, because the files it
1718/// appears in (`project.rs`, `project/tests_emit.rs`) are otherwise genuinely
1719/// TypeScript-producing.
1720///
1721/// **`.with_file_name(format!` found and added by Arc F's own item-4 investigation
1722/// (#1457):** `project.rs`'s `sibling_path` (`output_path.with_file_name(format!(
1723/// "{name}.{suffix}"))`) builds a sibling filesystem path the same way the two idioms
1724/// above do, but spelled with `.with_file_name(` — the prior substring match didn't
1725/// catch it, over-counting `ts_writes` by this one site.
1726fn is_path_construction_line(line: &str) -> bool {
1727 line.contains("PathBuf::from(format!")
1728 || line.contains(".join(format!")
1729 || line.contains(".with_file_name(format!")
1730}
1731
1732/// Relativises every path in [`rust_files`]'s output against `dir`, so [`ts_writes`]
1733/// and [`ts_any`]'s counting logic ([`ts_writes_violations`], [`ts_any_violations`])
1734/// takes the same `&[(PathBuf, String)]` shape [`production_std_fs_files`] does — an
1735/// in-memory file list a test can construct directly, per review of #1297 (a first cut
1736/// of these two probes took `root: &Path` and did its own walk, so nothing but the
1737/// drift gate actually exercised the exclusion logic; deleting a `continue` left every
1738/// test green).
1739fn rust_files_relative(dir: &Path) -> Vec<(PathBuf, String)> {
1740 rust_files(dir)
1741 .into_iter()
1742 .map(|(path, contents)| {
1743 let rel = path.strip_prefix(dir).unwrap_or(&path).to_path_buf();
1744 (rel, contents)
1745 })
1746 .collect()
1747}
1748
1749/// [`ts_writes`]'s counting logic, over an explicit `(relative path, contents)` list —
1750/// see [`rust_files_relative`] for why this isn't `root: &Path`.
1751///
1752/// **A real mistake this slice's own grounding found and fixed, not carried forward:**
1753/// an earlier survey (during phase 7's own track-opening research) characterised
1754/// `project/tests_emit.rs`'s 128 such sites as excludable "test-assertion strings" — the
1755/// same mischaracterisation `semantics-in-the-checker.md`'s own settling review caught
1756/// and corrected for a *different* probe (`emit_diagnostics`) on this same file: it is
1757/// `process_tests`/`process_integration_tests`, real production TypeScript-emission
1758/// code, not fixture noise, and none of its 128 sites fall inside its own single
1759/// `#[cfg(test)] mod tests { .. }` block. All 128 count here, less the one line that
1760/// genuinely builds a file path ([`is_path_construction_line`]).
1761///
1762/// **Known, accepted gap:** `project/tests_emit.rs`'s
1763/// `target_name: format!("system · {suite}")` builds a human-readable struct-field
1764/// label, not TypeScript text, and matches neither exclusion rule. A text-level scanner
1765/// has no cheap way to catch one field-name-specific site without a bespoke rule for it
1766/// alone — accepted as a one-site over-count, the same "known remaining gaps, out of
1767/// reach for a text-level scanner" discipline [`production_std_fs_files`] already
1768/// documents for a different probe.
1769fn ts_writes_violations(files: &[(PathBuf, String)]) -> usize {
1770 let mut count = 0usize;
1771 for (rel, contents) in files {
1772 if is_ts_writes_excluded_file(rel) {
1773 continue;
1774 }
1775 let lines: Vec<&str> = contents.lines().collect();
1776 let ranges = test_mod_ranges(&lines);
1777 for (i, line) in lines.iter().enumerate() {
1778 if in_test_range(i, &ranges) || is_line_comment(line) || is_path_construction_line(line)
1779 {
1780 continue;
1781 }
1782 if line.contains("write!") || line.contains("writeln!") || line.contains("format!") {
1783 count += 1;
1784 }
1785 }
1786 }
1787 count
1788}
1789
1790/// The trajectory's own phase-7 probe (`design/bynk-compiler-trajectory.md` §3):
1791/// "TypeScript-producing `write!` outside a printer". Never measured before this slice
1792/// (P7.0, #1296; see phase 7's own closing summary, `design/archive/retired-tracks.md`,
1793/// for the full retirement argument) — `bynk-ts` did not exist yet at measurement time,
1794/// so "outside a printer" reduced then to "in `bynk-emit`, outside a `Verbatim`
1795/// construction"; the `Verbatim` half of that exclusion was vacuous until P7.5 built the
1796/// type.
1797///
1798/// **Not "zero/closure"-shaped like this module's other twelve gated probes, and gated
1799/// anyway — a deliberate choice, not an inherited one.** The reading started at 1641 and
1800/// converged, over dozens of slices, to phase 7's own argued retirement floor, **809**
1801/// (ADR 0409; the full bucket-by-bucket accounting is in phase 7's own closing summary,
1802/// `design/archive/retired-tracks.md`) — never the literal 0 first proposed, nor a small
1803/// fixed number the way `ast_importers`/`emit_abi_shapes` are. Stays gated after
1804/// retirement, not deleted — a regression ratchet like `ast_importers` (floor 5) already
1805/// is: it moves on any `bynk-emit` PR that adds or removes a single `write!`/`writeln!`/
1806/// `format!` line anywhere in the crate — the same volatility #999 Decision D cites for
1807/// *not* gating `wildcard_arms` (311, ungated for exactly this reason) — but a floor this
1808/// track spent dozens of slices earning is worth catching a silent regression against,
1809/// the same trade `ast_importers` already made successfully across phase 6's 59 slices.
1810/// The churn cost is real and accepted, not overlooked: see ADR 0389/ADR 0390 for the
1811/// argument in full (review of #1297).
1812///
1813/// Counts `bynk-emit/src/**/*.rs` lines — excluding comments, `#[cfg(test)]` test-module
1814/// ranges, [`TS_WRITES_EXCLUDED_FILES`], and [`is_path_construction_line`] matches —
1815/// containing `write!`, `writeln!` or `format!`. See [`ts_writes_violations`] for the
1816/// counting logic itself.
1817fn ts_writes(root: &Path) -> Probe {
1818 let dir = root.join("bynk-emit/src");
1819 Probe {
1820 name: "ts_writes",
1821 gated: true,
1822 reads: ts_writes_violations(&rust_files_relative(&dir)).to_string(),
1823 }
1824}
1825
1826// --- Gated probe 11: ts_any -------------------------------------------------
1827
1828/// True if `line` (not a comment) violates R7.1's `TsType::Any` prohibition: an
1829/// `as any` cast, a bare `: any` type annotation, or `any` in generic type-argument
1830/// position (`Array<any>`, `Record<string, any[]>`, `Promise<any>`).
1831///
1832/// Six patterns, not `as any` alone, following three rounds of the same finding.
1833/// Round one (Q3, phase 7's own settling pass) found `as any` alone
1834/// under-counts R7.1 and added bare `: any`. Round two (review of #1297) found *that*
1835/// still under-counts: `bynk-emit/src/emitter/lower.rs`'s `joinOn`/`leftJoin`/`groupBy`
1836/// emit `const __h: Record<string, any[]> = {}` — `, any[]` contains neither `as any`
1837/// nor `: any`, so three live, production, TypeScript-emitting sites read as clean
1838/// under the round-one predicate. Widened to also match `<any`, `any>` and `any[]` —
1839/// each checked against the live tree for false positives (no non-`any`-typed English
1840/// word starts with `any` immediately after `<` or ends in `any` immediately before
1841/// `>`/`[]` anywhere in `bynk-emit/src` today) rather than assumed safe. Round three
1842/// (review of #1322) found a fourth spelling: once a site builds a real `bynk_ts::
1843/// TsType` node instead of writing TypeScript text directly, an emitted `any` no
1844/// longer appears as Rust-source `as any`/`: any` at all — `workers.rs`'s own
1845/// `TsType::named("any")` calls (#1321) emit the identical `payload as any`/
1846/// `let __who: any` text as before, byte-for-byte, but the *Rust spelling* that
1847/// produces it no longer matches any of the five text patterns above, so the probe
1848/// silently uncounted three real, still-live R7.1 residuals. Every later Arc C slice
1849/// converting an `any`-emitting `writeln!`/`format!` site the same way would keep
1850/// deflating this count the same way, so the fix generalises rather than special-
1851/// cases these three lines: match the construction spelling itself
1852/// (`named("any"`), not just raw emitted text.
1853///
1854/// Split out from [`ts_any_violations`] so a test can exercise the predicate directly,
1855/// without file I/O.
1856fn line_violates_ts_any(line: &str) -> bool {
1857 !is_line_comment(line)
1858 && (line.contains("as any")
1859 || line.contains(": any")
1860 || line.contains("<any")
1861 || line.contains("any>")
1862 || line.contains("any[]")
1863 || line.contains("named(\"any\""))
1864}
1865
1866/// [`ts_any`]'s counting logic, over an explicit `(relative path, contents)` list — see
1867/// [`rust_files_relative`] for why this isn't `root: &Path`.
1868fn ts_any_violations(files: &[(PathBuf, String)]) -> usize {
1869 let mut count = 0usize;
1870 for (rel, contents) in files {
1871 if is_ts_writes_excluded_file(rel) {
1872 continue;
1873 }
1874 let lines: Vec<&str> = contents.lines().collect();
1875 let ranges = test_mod_ranges(&lines);
1876 for (i, line) in lines.iter().enumerate() {
1877 if in_test_range(i, &ranges) {
1878 continue;
1879 }
1880 if line_violates_ts_any(line) {
1881 count += 1;
1882 }
1883 }
1884 }
1885 count
1886}
1887
1888/// Reference rule R7.1 (`design/bynk-greenfield-compiler.md` Part 7) — "the tree
1889/// contains no ... `TsType::Any`". Gated for the same reason [`ts_writes`] is (see its
1890/// own doc comment): started at 55 (not the settling review's estimated ~24 — ADR
1891/// 0390), converged over several slices to phase 7's own argued retirement floor, **26**
1892/// (ADR 0404; six already-argued families, none newly tractable — full accounting in
1893/// phase 7's own closing summary, `design/archive/retired-tracks.md`), and stays gated
1894/// after retirement as a regression ratchet, the same "I removed an `Any`" CI-checkable
1895/// claim every slice needed.
1896///
1897/// Counts `bynk-emit/src/**/*.rs` lines — excluding `#[cfg(test)]` test-module ranges
1898/// and [`TS_WRITES_EXCLUDED_FILES`] (the same files [`ts_writes`] excludes for producing
1899/// no TypeScript at all; an `any`-typed value there isn't R7.1's business either) —
1900/// matching [`line_violates_ts_any`]. Hand-written runtime `.ts` files under
1901/// `bynk-emit/runtime/` are out of scope by construction: [`rust_files`] only walks
1902/// `.rs` files, and R7.1 governs the emitted *tree*, not the hand-written runtime R7.7
1903/// separately covers.
1904fn ts_any(root: &Path) -> Probe {
1905 let dir = root.join("bynk-emit/src");
1906 Probe {
1907 name: "ts_any",
1908 gated: true,
1909 reads: ts_any_violations(&rust_files_relative(&dir)).to_string(),
1910 }
1911}
1912
1913// --- Gated probe 12: verbatim_origins ---------------------------------------
1914
1915/// P7.5 (#1307): distinct `bynk_ts::VerbatimOrigin` variants named in
1916/// `bynk-emit/src` — how many *families* of residual, not-yet-converted
1917/// emission remain, not their size (`verbatim_sites`, below, is the size).
1918/// Retired at an **argued floor**, named file-by-file the way `ast_importers`'s
1919/// floor of 5 was: **1** (ADR 0410) — only `NotYetConverted` has a live
1920/// production reference, at the same two sites `verbatim_sites`'s own floor
1921/// names permanent; full accounting in phase 7's own closing summary,
1922/// `design/archive/retired-tracks.md`. Read **0** at this slice's own
1923/// landing (`bynk-emit` built no `Verbatim` content yet, #1307's Decision C
1924/// — Arc C's own first slice is what gave this probe something to count);
1925/// stays gated after retirement as a regression ratchet.
1926///
1927/// Line-scans for `VerbatimOrigin::<Variant>` and counts distinct variant
1928/// names referenced, the same needle-scan shape [`hoist_sinks`] uses. A
1929/// known, accepted gap (review of #1308, finding 6): a bare `use
1930/// bynk_ts::VerbatimOrigin::Contracts;` followed by unqualified `Contracts`
1931/// elsewhere would undercount, since the needle is the qualified path. Not
1932/// worth a real-parser fix for an *argued-floor* probe (unlike
1933/// `verbatim_sites`'s own floor of exactly 0) — `bynk-emit`'s own existing
1934/// call-site style always qualifies (`TsStmt::verbatim(VerbatimOrigin::X,
1935/// …)`), so this is a theoretical undercount, not an observed one.
1936fn verbatim_origins(root: &Path) -> Probe {
1937 let dir = root.join("bynk-emit/src");
1938 Probe {
1939 name: "verbatim_origins",
1940 gated: true,
1941 reads: verbatim_origins_violations(&rust_files_relative(&dir)).to_string(),
1942 }
1943}
1944
1945/// [`verbatim_origins`]'s counting logic, over an explicit `(relative path,
1946/// contents)` list — see [`rust_files_relative`] for why this isn't `root:
1947/// &Path`. Excludes `#[cfg(test)]` ranges the same way [`ts_any_violations`]
1948/// does (review of #1308, finding 6): without this, one `bynk-emit` unit
1949/// test constructing a `VerbatimOrigin` for its own fixture pins this probe
1950/// above its argued floor permanently, for a reason that has nothing to do
1951/// with residual production emission.
1952fn verbatim_origins_violations(files: &[(PathBuf, String)]) -> usize {
1953 let needle = "VerbatimOrigin::";
1954 let mut variants: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1955 for (_, contents) in files {
1956 let lines: Vec<&str> = contents.lines().collect();
1957 let ranges = test_mod_ranges(&lines);
1958 for (i, line) in lines.iter().enumerate() {
1959 if in_test_range(i, &ranges) || is_line_comment(line) {
1960 continue;
1961 }
1962 let mut rest = *line;
1963 while let Some(idx) = rest.find(needle) {
1964 let after = &rest[idx + needle.len()..];
1965 let name: String = after
1966 .chars()
1967 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
1968 .collect();
1969 rest = &after[name.len()..];
1970 if !name.is_empty() {
1971 variants.insert(name);
1972 }
1973 }
1974 }
1975 }
1976 variants.len()
1977}
1978
1979// --- Gated probe 13: verbatim_sites -----------------------------------------
1980
1981/// P7.5 (#1307): distinct `TsStmt::verbatim(...)`/`TsExpr::VerbatimExpr(...)`
1982/// construction call sites in `bynk-emit/src`, line-scanned the same way
1983/// [`hoist_sinks`] counts `stmts: &mut Vec<String>` occurrences. Every call
1984/// site converting to a real tree node is what Arc C's own per-file slices
1985/// were actually for — `verbatim_origins` alone can't distinguish "3
1986/// variants, 12 residual call sites" from "3 variants, 900 residual call
1987/// sites, two files never decomposed"; this is what closes that gap. Retired
1988/// at an **argued floor** for the `TsStmt` half, not the flat 0 first
1989/// proposed: **2** (ADR 0399/ADR 0407, confirmed unchanged by the #1486
1990/// capstone) — `project.rs`'s adapter-binding copy loop (a foreign,
1991/// user-authored TypeScript payload) and its `runtime.ts` staging (a
1992/// committed npm build artifact), neither ever generated by `bynk-emit`;
1993/// full accounting in phase 7's own closing summary, `design/archive/
1994/// retired-tracks.md`.
1995///
1996/// #1539 widens the scan to the `TsExpr` half of the same escape hatch
1997/// (`TsExpr::VerbatimExpr`, closing the untagged-`Ident` gap the review
1998/// found) and moves the floor to **11** (the 2 permanent `TsStmt` sites plus
1999/// 9 residual `TsExpr` construction sites in `emit.rs` — a generic-typed
2000/// callee `Call`/`New` has no `type_args` field, a nested `As`-under-`As`
2001/// chain the printer's own operand-parenthesisation rule would mis-wrap, a
2002/// block-bodied ICU IIFE, and a `pred_condition_and_message`-style message
2003/// that a second `TsLit::Str` escaping pass would corrupt — see
2004/// `TsExpr::VerbatimExpr`'s own doc for the full list). Unlike the `TsStmt`
2005/// pair, these 9 are not argued-permanent the same way: each converts to a
2006/// real node the day `bynk-ts` gains the matching type-algebra piece
2007/// (`type_args`, a parenthesisation fix, …), so this half of the floor is
2008/// expected to keep shrinking, tracked here rather than assumed fixed. Read
2009/// **0** at this slice's own landing, same reason `verbatim_origins` did;
2010/// stays gated after retirement as a regression ratchet.
2011fn verbatim_sites(root: &Path) -> Probe {
2012 let dir = root.join("bynk-emit/src");
2013 Probe {
2014 name: "verbatim_sites",
2015 gated: true,
2016 reads: verbatim_sites_violations(&rust_files_relative(&dir)).to_string(),
2017 }
2018}
2019
2020/// [`verbatim_sites`]'s counting logic, over an explicit `(relative path,
2021/// contents)` list — see [`rust_files_relative`] for why this isn't `root:
2022/// &Path`. Excludes `#[cfg(test)]` ranges the same way [`ts_any_violations`]
2023/// does (review of #1308, finding 6): `verbatim_sites` is documented as
2024/// retiring at 0, so a residual construction site inside a test fixture
2025/// would pin it above zero permanently for a reason that has nothing to do
2026/// with production emission conversion.
2027fn verbatim_sites_violations(files: &[(PathBuf, String)]) -> usize {
2028 let needles = ["TsStmt::verbatim(", "TsExpr::VerbatimExpr("];
2029 let mut count = 0usize;
2030 for (_, contents) in files {
2031 let lines: Vec<&str> = contents.lines().collect();
2032 let ranges = test_mod_ranges(&lines);
2033 for (i, line) in lines.iter().enumerate() {
2034 if in_test_range(i, &ranges) || is_line_comment(line) {
2035 continue;
2036 }
2037 if needles.iter().any(|needle| line.contains(needle)) {
2038 count += 1;
2039 }
2040 }
2041 }
2042 count
2043}
2044
2045/// Named identifiers imported from the compiler-generated firstparty/runtime relative
2046/// modules (`./bynk.js`, `./runtime.js`, `./bynk/locale/types.js`, `./cloudflare.js`,
2047/// or their `../` forms) — `import type { A, B }`/`import { A, B }` braces, stripping
2048/// `type ` markers and `X as Y` aliases (keeping the imported name, not the local one,
2049/// since the allowlists are about what's referenced, not what it's called locally).
2050fn ts_named_imports_from_runtime_modules(src: &str) -> Vec<String> {
2051 let mut out = Vec::new();
2052 for line in src.lines() {
2053 let line = line.trim();
2054 if !line.starts_with("import") {
2055 continue;
2056 }
2057 let is_runtime_module = ["\"./bynk.js\"", "\"./runtime.js\"", "\"../runtime.js\""]
2058 .iter()
2059 .any(|m| line.ends_with(&format!("from {m};")))
2060 || line.contains("__bynk_runtime/")
2061 || line.contains("bynk/locale/types.js")
2062 || line.contains("cloudflare.js");
2063 if !is_runtime_module {
2064 continue;
2065 }
2066 let Some(open) = line.find('{') else { continue };
2067 let Some(close) = line.find('}') else {
2068 continue;
2069 };
2070 for part in line[open + 1..close].split(',') {
2071 let part = part.trim().trim_start_matches("type ").trim();
2072 if part.is_empty() {
2073 continue;
2074 }
2075 let imported = part.split(" as ").next().unwrap_or(part).trim();
2076 out.push(imported.to_string());
2077 }
2078 }
2079 out
2080}
2081
2082// --- Gated probe 14: incremental_query_types --------------------------------
2083
2084/// Phase 8's own completion criterion (`design/bynk-compiler-trajectory.md` §3,
2085/// "keystroke-to-diagnostic latency by query level"), settled by #1509 (Q5, ADR 0414;
2086/// `design/tracks/incrementality.md` §5) as a one-time **existence** proof, not a count
2087/// trending toward a floor the way every other gated probe in this module is shaped —
2088/// R3.13/R3.14 describe a property to construct, not a defect to exhaust, so a
2089/// shrinking count would be the wrong shape regardless of how it was tuned.
2090///
2091/// **Re-settled by #1537 (2 September 2026), after the 30 August post-restructuring
2092/// review found the probe could not tell adoption from existence.** Phase 8 built all
2093/// four R3.13 levels; only the file level (P8.4's shared parse cache) and the unit
2094/// level's *proof* (P8.2's stability test over `UnitSignature`) had a consumer. The
2095/// definition level (`Body(DefId)`/`TypeOf(DefId)`, 816 lines) and the project level
2096/// (`ProjectGraph`, 174 lines) were reachable only from their own tests, with no
2097/// scheduler to call them and — per R3.15 and #1523 — no trigger yet for one. Both
2098/// were deleted rather than left "available but unwired" (P5), the same decision the
2099/// IR cutover (#1542) reached for phase 6's expression IR. This probe now certifies
2100/// the decision, in both directions:
2101///
2102/// 1. **Unit level** — `struct UnitSignature` exists as real code in `bynk-check`
2103/// (P8.1, ADR 0412). It is the R3.14 firewall's own specification, and the one
2104/// phase 8 artefact #1523's trigger presupposes; it stays as a *proof*, not a
2105/// production path (its only reader is clause 3's test), argued in #1537's ADR.
2106/// 2. **Shared cache** — the file-level parse cache has migrated off
2107/// `PROJECT_UNIT_CACHE` (`bynk-ide/src/completion.rs`) onto one shared,
2108/// `bynk-project`-owned cache (P8.4, ADR 0413). Checked two ways: the old static
2109/// gone from `bynk-ide/src`, *and* some cache-shaped `static`/`struct` present in
2110/// `bynk-project/src` — absence alone would read "migrated" for a bare deletion.
2111/// 3. **Stability test** — some `#[test]` under `bynk-check/tests/` proves
2112/// `UnitSignature`'s stability under a body edit (P8.2): any test name containing
2113/// both `unit_signature` and `stab`.
2114/// 4. **Definition and project levels absent** — no `struct ProjectGraph`, and no
2115/// `DefId`-keyed `fn body(`/`fn type_of(`, in **any** workspace crate's `src/`
2116/// ([`workspace_crate_src_files`], the same walk `unconsumed_ir_items` uses, minus
2117/// `xtask` itself, whose source spells the needles) — not just the two crates
2118/// phase 8 landed them in, since R3.13's own table
2119/// assigns `DefId` bindings to a `bynk-resolve` crate that does not exist yet and
2120/// a rebuild might put them there (review of #1582). `checker.rs`'s own
2121/// pre-existing per-expression `type_of`, which has no `DefId` parameter, does
2122/// not count — the false positive #1510's first run caught, now with the opposite
2123/// consequence. This clause is what makes the probe a gate on #1537 rather than a
2124/// memorial: re-adding either level changes the committed reading and fails the
2125/// currency test, so it needs a consumer and a re-settling — the trigger
2126/// R3.15/#1523 names. **What the gate does not see, stated rather than asserted
2127/// away:** a `DefId` parameter wrapped onto the line after `fn body(` (the
2128/// same-line rule [`defid_query_fn_present`] documents), or a query function under
2129/// any other name. Text-level, like every probe in this file.
2130///
2131/// Every clause is a static read of the tree (never a nested build or test run — see
2132/// [`unit_signature_present`]'s own doc comment for why a "does the stability test
2133/// *pass*" clause was deliberately rejected, #1510's own review-shaped framing).
2134fn incremental_query_types(root: &Path) -> Probe {
2135 let check_src = rust_files(&root.join("bynk-check/src"));
2136 let project_src = rust_files(&root.join("bynk-project/src"));
2137 let ide_src = rust_files(&root.join("bynk-ide/src"));
2138 let check_tests = rust_files(&root.join("bynk-check/tests"));
2139
2140 let unit_present = unit_signature_present(&check_src);
2141 let cache_migrated = shared_cache_migrated(&ide_src, &project_src);
2142 let test_present = stability_test_present(&check_tests);
2143 // Every workspace crate but this one: the harness's own source spells the
2144 // needles it scans for (in these very functions and their tests), so it
2145 // would read as a re-add of both levels on every run.
2146 let workspace_src: Vec<(PathBuf, String)> = workspace_crate_src_files(root)
2147 .into_iter()
2148 .filter(|(krate, _, _)| krate != "xtask")
2149 .map(|(_, path, contents)| (path, contents))
2150 .collect();
2151 let readded = deleted_levels_present(&workspace_src);
2152
2153 let reads = format!(
2154 "unit_signature {}; shared_cache {}; stability_test {}; definition/project levels {}",
2155 if unit_present { "present" } else { "absent" },
2156 if cache_migrated {
2157 "migrated"
2158 } else {
2159 "not migrated (PROJECT_UNIT_CACHE still bynk-ide-local)"
2160 },
2161 if test_present { "present" } else { "absent" },
2162 if readded.is_empty() {
2163 "absent (deleted by #1537)".to_string()
2164 } else {
2165 format!(
2166 "re-added ({}) — need a consumer and a re-settling of #1537",
2167 readded.join(", ")
2168 )
2169 },
2170 );
2171 Probe {
2172 name: "incremental_query_types",
2173 gated: true,
2174 reads,
2175 }
2176}
2177
2178/// Clause 1 of [`incremental_query_types`]: does `struct UnitSignature` exist as real
2179/// code (not a comment or doc prose) in `bynk-check`? The same "grep for the real
2180/// identifier, not the doc claim" discipline `design/tracks/incrementality.md` §1 used
2181/// to measure this reading as zero at settling. Deliberately *not* a "does P8.2's
2182/// fixture pass" check: every gated probe here is a static read of the tree, computed
2183/// from inside `xtask/tests/greenfield_status.rs`'s own `#[test]`; shelling out to
2184/// `cargo test` from inside a running `cargo test` is the identical nested-invocation
2185/// cost [`wildcard_arms`] (the one probe that shells out, and stays trend-only for
2186/// exactly this reason) avoids.
2187fn unit_signature_present(check_src: &[(PathBuf, String)]) -> bool {
2188 any_real_code_line(check_src, "struct UnitSignature")
2189}
2190
2191/// Clause 4 of [`incremental_query_types`]: which of the two levels #1537 deleted are
2192/// back anywhere in `src` — `ProjectGraph` as a real struct, or a `DefId`-keyed
2193/// `body`/`type_of` query function ([`defid_query_fn_present`]). Empty is the
2194/// committed reading; any entry changes the table and fails the currency gate, which
2195/// is the point.
2196fn deleted_levels_present(src: &[(PathBuf, String)]) -> Vec<&'static str> {
2197 let mut found = Vec::new();
2198 if any_real_code_line(src, "struct ProjectGraph") {
2199 found.push("ProjectGraph");
2200 }
2201 if defid_query_fn_present(src, "fn body(") {
2202 found.push("Body");
2203 }
2204 if defid_query_fn_present(src, "fn type_of(") {
2205 found.push("TypeOf");
2206 }
2207 found
2208}
2209
2210/// A `fn_needle`-matching signature line that *also* names `DefId` on the same line —
2211/// not just `fn_needle` alone. **A real, empirically-confirmed false positive this
2212/// slice's own first run caught, not a hypothetical:** `bynk-check/src/checker.rs`
2213/// already has a `pub(crate) fn type_of(expr: &Expr, expected: Option<TyId>, ctx: &mut
2214/// Ctx) -> Option<TyId>` — real, pre-existing, ordinary per-expression type-inference
2215/// plumbing that predates this whole track and has nothing to do with R3.13's
2216/// `DefId`-keyed query — a naive `fn type_of(` scan reads this as `TypeOf` already
2217/// existing on the very first run, before P8.5 does any work at all. Requiring
2218/// `DefId` on the same signature line is a real, if narrow, precision fix: it correctly
2219/// reads false against `checker.rs`'s own `type_of` today, and correctly flips true
2220/// once P8.5 lands a real `DefId`-keyed function, whatever it ends up calling it, as
2221/// long as the parameter appears on the `fn` line itself (a wrapped multi-line
2222/// signature would need widening this scan window — not needed for any function in
2223/// the tree today).
2224fn defid_query_fn_present(files: &[(PathBuf, String)], fn_needle: &str) -> bool {
2225 files.iter().any(|(_, contents)| {
2226 contents.lines().any(|line| {
2227 !is_line_comment(line) && line.contains(fn_needle) && line.contains("DefId")
2228 })
2229 })
2230}
2231
2232/// Whether the file-level parse cache has migrated off `bynk-ide`'s own
2233/// `PROJECT_UNIT_CACHE` onto some shared cache in `bynk-project` — see
2234/// [`incremental_query_types`]'s own doc comment (clause 2) for why *both* halves are
2235/// checked: absence from `bynk-ide` alone cannot distinguish a real migration from a
2236/// bare rename or deletion with nothing shared put in its place. The needle is
2237/// anchored on the `static` declaration line (`static PROJECT_UNIT_CACHE`), not a bare
2238/// substring, so `PROJECT_UNIT_CACHE_CAP` (a real, unrelated `const` in
2239/// `bynk-ide/src/completion.rs`) can't hold this false on its own.
2240fn shared_cache_migrated(ide_src: &[(PathBuf, String)], project_src: &[(PathBuf, String)]) -> bool {
2241 !any_real_code_line(ide_src, "static PROJECT_UNIT_CACHE")
2242 && cache_shaped_item_present(project_src)
2243}
2244
2245/// Whether `bynk-project/src` has a `static`/`struct` item whose name mentions "cache"
2246/// (case-insensitive) — the only crate-boundary-checkable proxy for "some shared cache
2247/// now lives where P8.4 is meant to put it," until that slice pins the real identifier
2248/// down. Deliberately loose, the same "exact name not yet proposed" reasoning
2249/// [`stability_test_present`] already uses.
2250fn cache_shaped_item_present(project_src: &[(PathBuf, String)]) -> bool {
2251 project_src.iter().any(|(_, contents)| {
2252 contents.lines().any(|line| {
2253 if is_line_comment(line) {
2254 return false;
2255 }
2256 let trimmed = line.trim_start();
2257 let is_item = trimmed.starts_with("static ")
2258 || trimmed.starts_with("pub static ")
2259 || trimmed.starts_with("struct ")
2260 || trimmed.starts_with("pub struct ");
2261 is_item && line.to_lowercase().contains("cache")
2262 })
2263 })
2264}
2265
2266/// Whether any `#[test]` fn under `bynk-check/tests/` looks like P8.2's own
2267/// body-edit-stability property test — see [`incremental_query_types`]'s own doc
2268/// comment (clause 3) for why the name match (`unit_signature` + `stab`) is
2269/// deliberately loose. The `#[test]` attribute itself is *not* loose: it must be on the
2270/// matching `fn` line or on a contiguous run of attribute lines directly above it, so a
2271/// same-named non-test helper (a fixture builder, say) can't satisfy this clause.
2272fn stability_test_present(check_tests: &[(PathBuf, String)]) -> bool {
2273 check_tests.iter().any(|(_, contents)| {
2274 let lines: Vec<&str> = contents.lines().collect();
2275 lines.iter().enumerate().any(|(i, line)| {
2276 if is_line_comment(line) {
2277 return false;
2278 }
2279 let lower = line.to_lowercase();
2280 let name_matches =
2281 lower.contains("fn ") && lower.contains("unit_signature") && lower.contains("stab");
2282 if !name_matches {
2283 return false;
2284 }
2285 line.contains("#[test]")
2286 || lines[..i]
2287 .iter()
2288 .rev()
2289 .take_while(|l| l.trim_start().starts_with('#'))
2290 .any(|l| l.trim() == "#[test]")
2291 })
2292 })
2293}
2294
2295/// Whether any line in `files` (excluding comments) contains `needle` — the shared
2296/// existence-check primitive [`unit_signature_present`]/[`deleted_levels_present`]/
2297/// [`shared_cache_migrated`] all use.
2298fn any_real_code_line(files: &[(PathBuf, String)], needle: &str) -> bool {
2299 files.iter().any(|(_, contents)| {
2300 contents
2301 .lines()
2302 .any(|line| !is_line_comment(line) && line.contains(needle))
2303 })
2304}
2305
2306// --- Gated probe 15: unconsumed_ir_items ------------------------------------
2307
2308/// Slice D3 of #1542 (`design/archive/retired-tracks.md`, the IR cutover's own
2309/// closing summary): the adoption probe the 30 August 2026 post-restructuring
2310/// review (`design/reviews/2026-08-30-post-restructuring-review.md`, Part 5 §8)
2311/// asked for — for each `pub` item in `bynk-ir/src` and `bynk-lower/src`, does
2312/// a production call site exist outside the owning crate and outside a test
2313/// module? That review found phase 6 had shipped an expression IR nothing
2314/// consumed (fifteen `bynk-lower` entry points, twenty-one `bynk-ir` types),
2315/// invisible to every existing gate because each of them certifies that a name
2316/// *exists* in a directory, not that anything *reads* it. Scoped to the two IR
2317/// crates because their entire purpose is to be consumed elsewhere: a `pub`
2318/// item in either with no reader in another crate is, by construction, either
2319/// dead or a second path waiting to be wired in — the P5 failure
2320/// (`bynk-greenfield-compiler.md`) both phases 6 and 8 reproduced.
2321///
2322/// Reads **0** at its own landing, by construction: Slices D0–D2 deleted every
2323/// unconsumed item (D1 also demoted the two crate-internal helpers,
2324/// `lower_fn_sig_ir_from_types`/`lower_op_sig_ir_from_commons`, that would
2325/// otherwise have read as 2). Gated as a ratchet: a new `pub` item in either
2326/// crate with no consumer moves it off zero and fails
2327/// `greenfield_status_table_is_current`, so the "available but unwired" state
2328/// cannot land silently again. The reading names the offending items so the
2329/// failure is actionable, not just a count.
2330///
2331/// **What counts as a consumer.** A non-comment line, outside any `#[cfg(test)]
2332/// mod` range ([`test_mod_ranges`]), in a workspace crate's `src/` that is
2333/// **neither owner crate**, containing the item's name as a whole word. The
2334/// two owners do not vouch for each other, on purpose (review of this slice's
2335/// own PR, #1581): run against pre-D0 `main` with only the owning crate
2336/// excluded, every one of phase 6's twenty-one unconsumed `bynk-ir` types
2337/// would have read as consumed, because `bynk-lower`'s own unconsumed
2338/// constructors named them — the probe would have missed half the surface it
2339/// was built to see. With both excluded, its first honest run read 5
2340/// (`IndexIr` and the four `MUTATING_*_OPS` tables, read only from
2341/// `bynk-lower`), resolved by inlining the alias and moving the tables beside
2342/// their one reader rather than arguing a floor. Text-level, like every probe
2343/// in this file — a name that happens to be shared with an unrelated item in
2344/// another crate would read as consumed (a false negative for the ratchet,
2345/// never a false positive that blocks a PR), accepted the same way
2346/// [`ts_writes`]'s own known over-count is. Items are `pub` at column zero
2347/// only — `fn`, `struct`, `enum`, `type`, `const`, `static`, `trait`, `union`,
2348/// with any `async`/`unsafe`/`const`/`extern` qualifier on a `fn` — see
2349/// [`column_zero_pub_item_name`]: `pub(crate)` is by definition not offered
2350/// to another crate, a `pub` item nested inside an `impl` block is reachable
2351/// only through its owner (which is what gets counted), and `pub use`/`pub
2352/// mod` re-export rather than declare.
2353fn unconsumed_ir_items(root: &Path) -> Probe {
2354 let owners = ["bynk-ir", "bynk-lower"];
2355 let mut owner_files = Vec::new();
2356 for owner in owners {
2357 for (path, contents) in rust_files_relative(&root.join(owner).join("src")) {
2358 owner_files.push((owner.to_string(), path, contents));
2359 }
2360 }
2361 let consumer_files = workspace_crate_src_files(root);
2362 let unconsumed = unconsumed_pub_items(&owners, &owner_files, &consumer_files);
2363 let reads = if unconsumed.is_empty() {
2364 "0".to_string()
2365 } else {
2366 format!("{} ({})", unconsumed.len(), unconsumed.join(", "))
2367 };
2368 Probe {
2369 name: "unconsumed_ir_items",
2370 gated: true,
2371 reads,
2372 }
2373}
2374
2375/// Every `.rs` file under `<crate>/src` for every workspace crate — each entry
2376/// tagged with its crate directory name so [`unconsumed_pub_items`] can exclude
2377/// an item's own crate. A workspace crate is any immediate child of `root` with
2378/// both a `Cargo.toml` and a `src/`, the same shape every `members` entry in the
2379/// root manifest has; reading the manifest itself would add a TOML parse for no
2380/// gain in precision.
2381fn workspace_crate_src_files(root: &Path) -> Vec<(String, PathBuf, String)> {
2382 let mut out = Vec::new();
2383 let Ok(entries) = std::fs::read_dir(root) else {
2384 return out;
2385 };
2386 let mut crates: Vec<PathBuf> = entries
2387 .flatten()
2388 .map(|e| e.path())
2389 .filter(|p| p.join("Cargo.toml").is_file() && p.join("src").is_dir())
2390 .collect();
2391 crates.sort();
2392 for krate in crates {
2393 let name = krate
2394 .file_name()
2395 .map(|n| n.to_string_lossy().into_owned())
2396 .unwrap_or_default();
2397 for (path, contents) in rust_files_relative(&krate.join("src")) {
2398 out.push((name.clone(), path, contents));
2399 }
2400 }
2401 out
2402}
2403
2404/// [`unconsumed_ir_items`]'s counting logic over explicit `(crate, relative
2405/// path, contents)` lists — see [`rust_files_relative`] for why this isn't
2406/// `root: &Path`. Returns `crate::item` for every column-zero `pub` item in
2407/// `owner_files` that no non-comment, non-test line in a file belonging to a
2408/// crate outside `owners` names as a whole word; sorted and deduplicated (a
2409/// name declared in two files of one crate is one item, not two), so the
2410/// reading is stable across runs and across a crate being split into modules.
2411///
2412/// Each consumer file is split and its `#[cfg(test)]` ranges computed once,
2413/// up front, not once per candidate item (review of #1581): this runs under
2414/// `cargo test --workspace` on every Rust-touching PR, and the failure path —
2415/// an item with no consumer — is exactly the one that scans every file.
2416fn unconsumed_pub_items(
2417 owners: &[&str],
2418 owner_files: &[(String, PathBuf, String)],
2419 consumer_files: &[(String, PathBuf, String)],
2420) -> Vec<String> {
2421 let mut items: Vec<(String, String)> = Vec::new();
2422 for (krate, _, contents) in owner_files {
2423 let lines: Vec<&str> = contents.lines().collect();
2424 let ranges = test_mod_ranges(&lines);
2425 for (i, line) in lines.iter().enumerate() {
2426 if in_test_range(i, &ranges) {
2427 continue;
2428 }
2429 if let Some(name) = column_zero_pub_item_name(line) {
2430 items.push((krate.clone(), name.to_string()));
2431 }
2432 }
2433 }
2434 items.sort();
2435 items.dedup();
2436
2437 // Production, non-owner lines only — preprocessed once.
2438 let consumer_lines: Vec<&str> = consumer_files
2439 .iter()
2440 .filter(|(krate, _, _)| !owners.contains(&krate.as_str()))
2441 .flat_map(|(_, _, contents)| {
2442 let lines: Vec<&str> = contents.lines().collect();
2443 let ranges = test_mod_ranges(&lines);
2444 lines
2445 .iter()
2446 .enumerate()
2447 .filter(|(i, line)| !in_test_range(*i, &ranges) && !is_line_comment(line))
2448 .map(|(_, line)| *line)
2449 .collect::<Vec<_>>()
2450 })
2451 .collect();
2452
2453 items
2454 .iter()
2455 .filter(|(_, name)| !consumer_lines.iter().any(|line| contains_word(line, name)))
2456 .map(|(owner, name)| format!("{owner}::{name}"))
2457 .collect()
2458}
2459
2460/// The name of a column-zero `pub` item declaration, if `line` is one: `pub`
2461/// followed by `fn`, `struct`, `enum`, `type`, `const`, `static`, `trait` or
2462/// `union`, where a `fn` may carry `async`/`unsafe`/`const`/`extern "…"`
2463/// qualifiers in any order. `pub(crate)`/`pub(super)` do not qualify (they
2464/// are not offered to other crates), and neither does anything indented (a
2465/// method or associated item, reachable only through its owner), nor `pub
2466/// use`/`pub mod` (re-exports and module declarations, not items).
2467fn column_zero_pub_item_name(line: &str) -> Option<&str> {
2468 let mut rest = line.strip_prefix("pub ")?;
2469 // `pub const fn` / `pub async unsafe fn` / `pub unsafe extern "C" fn` …:
2470 // peel qualifiers until the item keyword is exposed. A bare `pub const X`
2471 // is a constant, not a qualifier, so `const` only peels when a `fn`
2472 // (possibly behind further qualifiers) follows it.
2473 loop {
2474 if let Some(r) = rest
2475 .strip_prefix("async ")
2476 .or_else(|| rest.strip_prefix("unsafe "))
2477 {
2478 rest = r;
2479 continue;
2480 }
2481 if let Some(r) = rest.strip_prefix("extern ") {
2482 // `extern "C" fn` — skip the ABI string if present.
2483 let r = r.trim_start();
2484 let r = if let Some(after_quote) = r.strip_prefix('"') {
2485 after_quote
2486 .find('"')
2487 .map(|q| after_quote[q + 1..].trim_start())
2488 .unwrap_or(r)
2489 } else {
2490 r
2491 };
2492 rest = r;
2493 continue;
2494 }
2495 if let Some(r) = rest.strip_prefix("const ")
2496 && (r.starts_with("fn ")
2497 || r.starts_with("async ")
2498 || r.starts_with("unsafe ")
2499 || r.starts_with("extern "))
2500 {
2501 rest = r;
2502 continue;
2503 }
2504 break;
2505 }
2506 let rest = [
2507 "fn ", "struct ", "enum ", "type ", "const ", "static ", "trait ", "union ",
2508 ]
2509 .iter()
2510 .find_map(|kw| rest.strip_prefix(kw))?;
2511 let end = rest
2512 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
2513 .unwrap_or(rest.len());
2514 (end > 0).then(|| &rest[..end])
2515}
2516
2517/// Does `line` contain `word` as a whole identifier — not as a prefix or
2518/// suffix of a longer one (`IrExpr` inside `IrExprKind` must not count)?
2519fn contains_word(line: &str, word: &str) -> bool {
2520 let bytes = line.as_bytes();
2521 let mut from = 0;
2522 while let Some(pos) = line[from..].find(word) {
2523 let start = from + pos;
2524 let end = start + word.len();
2525 let before_ok = start == 0 || !is_ident_byte(bytes[start - 1]);
2526 let after_ok = end == bytes.len() || !is_ident_byte(bytes[end]);
2527 if before_ok && after_ok {
2528 return true;
2529 }
2530 from = start + 1;
2531 }
2532 false
2533}
2534
2535fn is_ident_byte(b: u8) -> bool {
2536 b.is_ascii_alphanumeric() || b == b'_'
2537}
2538
2539// --- Reported probe 1: wildcard_arms --------------------------------------
2540
2541/// R2.12. `clippy::wildcard_enum_match_arm` diagnostics, forced on via `-W` so the
2542/// count is real from day one and doesn't wait on `workspace_lints`/T0.3 (#999 Decision
2543/// C — delegating to clippy's own type-aware pass, rather than a hand-rolled scan for
2544/// "compiler-owned enum", so the probe and the enforcement mechanism can never
2545/// disagree). A count, not a boolean — moves on nearly every match statement anyone
2546/// writes, so it is reported, not gated (#999 Decision D).
2547fn wildcard_arms(root: &Path) -> Probe {
2548 let reads = match run_clippy_wildcard_scan(root) {
2549 Ok(n) => n.to_string(),
2550 Err(e) => format!("error running clippy: {e}"),
2551 };
2552 Probe {
2553 name: "wildcard_arms",
2554 gated: false,
2555 reads,
2556 }
2557}
2558
2559/// Runs clippy with the lint forced on and parses the NDJSON output properly —
2560/// **not** a substring count. A single `wildcard_enum_match_arm` diagnostic's JSON
2561/// repeats the lint name several times (the `code` field, the human-readable message,
2562/// the `#[warn(...)]` note, and the `rendered` field duplicating the whole thing as
2563/// text), so `stdout.matches("wildcard_enum_match_arm").count()` overcounts by roughly
2564/// 3x — caught by cross-checking this probe's own first run against a real JSON parse
2565/// (296 real diagnostics, not the naive scan's 888).
2566///
2567/// Checks the process exit status: a forced `-W` (not `-D`) never fails the build on
2568/// account of the lint itself, so a non-zero exit means clippy genuinely could not run
2569/// (a compile error elsewhere, a missing toolchain component, offline with no cached
2570/// index) — in which case stdout carries no `compiler-message` lines and a silent
2571/// success would report a false, and indistinguishable, `0`. This probe is reported,
2572/// not gated, precisely so an honest "couldn't measure" surfaces loudly here rather
2573/// than being read as "closed."
2574fn run_clippy_wildcard_scan(root: &Path) -> std::io::Result<usize> {
2575 let output = Command::new("cargo")
2576 .args([
2577 "clippy",
2578 "--workspace",
2579 "--message-format=json",
2580 "--",
2581 "-W",
2582 "clippy::wildcard_enum_match_arm",
2583 ])
2584 .current_dir(root)
2585 .output()?;
2586 if !output.status.success() {
2587 return Err(std::io::Error::other(format!(
2588 "cargo clippy exited with {}: {}",
2589 output.status,
2590 String::from_utf8_lossy(&output.stderr).trim()
2591 )));
2592 }
2593 let stdout = String::from_utf8_lossy(&output.stdout);
2594 let mut count = 0usize;
2595 for line in stdout.lines() {
2596 let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
2597 continue;
2598 };
2599 if value.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") {
2600 continue;
2601 }
2602 let code = value.pointer("/message/code/code").and_then(|c| c.as_str());
2603 if code == Some("clippy::wildcard_enum_match_arm") {
2604 count += 1;
2605 }
2606 }
2607 Ok(count)
2608}
2609
2610// --- Reported probe 2: keep_in_sync ---------------------------------------
2611
2612/// P2 (trend only). Comments across the workspace containing "in sync", "mirrors",
2613/// "parity", or "must match" — each one names a rule the compiler cannot teach itself
2614/// and must be taught in review, every time.
2615fn keep_in_sync(root: &Path) -> Probe {
2616 let phrases = ["in sync", "mirrors", "parity", "must match"];
2617 let mut count = 0usize;
2618 for dir in top_level_crate_dirs(root) {
2619 for (_, contents) in rust_files(&dir.join("src")) {
2620 for line in contents.lines() {
2621 if is_line_comment(line) {
2622 let lower = line.to_lowercase();
2623 if phrases.iter().any(|p| lower.contains(p)) {
2624 count += 1;
2625 }
2626 }
2627 }
2628 }
2629 }
2630 Probe {
2631 name: "keep_in_sync",
2632 gated: false,
2633 reads: count.to_string(),
2634 }
2635}
2636
2637// --- Reported probe 3: test_density ---------------------------------------
2638
2639/// R11.1, and §3.4's phase-3 trigger. Per crate: (lines inside `#[test]` fn bodies,
2640/// plus lines inside `#[cfg(test)] mod` blocks outside those fns) ÷ (non-blank,
2641/// non-comment lines under that crate's `src/`) — #999 Decision F's definition,
2642/// written down precisely because an undefined "ratio" is exactly the ambiguity that
2643/// produced the track doc §9's four-row ambiguity.
2644fn test_density(root: &Path) -> Probe {
2645 let mut parts = Vec::new();
2646 for dir in top_level_crate_dirs(root) {
2647 let name = dir.file_name().unwrap().to_string_lossy().to_string();
2648 let src_dir = dir.join("src");
2649 let mut test_lines = 0usize;
2650 let mut code_lines = 0usize;
2651 for (_, contents) in rust_files(&src_dir) {
2652 let lines: Vec<&str> = contents.lines().collect();
2653 let ranges = test_mod_ranges(&lines);
2654 for (i, line) in lines.iter().enumerate() {
2655 let is_blank_or_comment = line.trim().is_empty() || is_line_comment(line);
2656 if !is_blank_or_comment {
2657 code_lines += 1;
2658 }
2659 if in_test_range(i, &ranges) && !is_blank_or_comment {
2660 test_lines += 1;
2661 }
2662 }
2663 }
2664 if code_lines > 0 {
2665 let ratio = 100.0 * test_lines as f64 / code_lines as f64;
2666 parts.push(format!("{name}={ratio:.1}%"));
2667 }
2668 }
2669 Probe {
2670 name: "test_density",
2671 gated: false,
2672 reads: parts.join(", "),
2673 }
2674}
2675
2676// --- diagnostic_coverage ---------------------------------------------------
2677
2678/// #1662 (track #1648, G2). Registry codes that some test **asserts**, out of all
2679/// `bynk_syntax::diagnostics::REGISTRY` codes, and the count no test asserts.
2680///
2681/// Static, so it runs inside this harness: the review that set the baseline
2682/// (2026-10-01, #1647 Part 4) instrumented `CompileError::new` across a full
2683/// `cargo test --workspace`, which this harness cannot afford. Asserted is a
2684/// subset of produced (a passing test that names a code saw it), so driving the
2685/// unasserted count to the argued floor meets "every reachable code is produced
2686/// by a test" a fortiori.
2687///
2688/// A code counts as asserted when it appears in:
2689/// - line 1 of a negative fixture's `expected_error.txt` (line 2 is a message
2690/// substring and may quote other codes);
2691/// - any other non-`.bynk` file under a crate's `tests/` directory (test
2692/// sources, expected-diagnostics files, JSON goldens). `.bynk` sources are
2693/// skipped, because their comments often name the code they provoke, and Rust
2694/// sources are read with their `//` comments removed, for the same reason;
2695/// - a `#[cfg(test)] mod` block in a crate's `src/`, comments removed;
2696/// - a blessed diagnostic transcript, `site/src/diagnostics/*.txt`.
2697///
2698/// Gated at the argued floor of **4** (#1662 Decision B). Each of the four is
2699/// emitted where no compiler test can reach it:
2700/// - `bynk.deploy.contract_skew`: `bynk deploy`, against a live deployment's
2701/// lock;
2702/// - `bynk.project.read_failed`: only when a host's file overlay omits a
2703/// discovered file, which neither the CLI nor the LSP does;
2704/// - `bynk.target.vendor_conflict`: needs platform-native capabilities from two
2705/// platforms, and only Cloudflare ships any today (the decision function is
2706/// unit-tested);
2707/// - `bynk.wasm.strip_failed`: stripping the compiler's own emitted TypeScript,
2708/// which fails only on an emitter bug.
2709///
2710/// `cargo xtask greenfield-status --list-unasserted` prints the codes.
2711pub fn unasserted_codes(root: &Path) -> Vec<&'static str> {
2712 let registry: BTreeSet<&'static str> = bynk_syntax::diagnostics::REGISTRY
2713 .iter()
2714 .map(|d| d.code)
2715 .collect();
2716 let mut asserted: BTreeSet<String> = BTreeSet::new();
2717 let mut note = |text: &str| {
2718 for code in registry_tokens(text) {
2719 asserted.insert(code);
2720 }
2721 };
2722 for krate in top_level_crate_dirs(root) {
2723 for (path, contents) in text_files(&krate.join("tests")) {
2724 if path.extension().is_some_and(|e| e == "bynk") {
2725 continue;
2726 }
2727 if path.file_name().is_some_and(|n| n == "expected_error.txt") {
2728 note(
2729 contents
2730 .lines()
2731 .find(|l| !l.trim().is_empty())
2732 .unwrap_or(""),
2733 );
2734 } else if path.extension().is_some_and(|e| e == "rs") {
2735 note(&strip_line_comments(&contents));
2736 } else {
2737 note(&contents);
2738 }
2739 }
2740 for (_, contents) in rust_files(&krate.join("src")) {
2741 let lines: Vec<&str> = contents.lines().collect();
2742 for (start, end) in test_mod_ranges(&lines) {
2743 note(&strip_line_comments(&lines[start..=end].join("\n")));
2744 }
2745 }
2746 }
2747 for (path, contents) in text_files(&root.join("site/src/diagnostics")) {
2748 if path.extension().is_some_and(|e| e == "txt") {
2749 note(&contents);
2750 }
2751 }
2752 registry
2753 .into_iter()
2754 .filter(|c| !asserted.contains(*c))
2755 .collect()
2756}
2757
2758/// `src` with every `//` comment (including `///` and `//!` doc comments)
2759/// removed, so a code named only in prose does not count as asserted. A `//`
2760/// inside a string literal is kept; block comments are rare enough in this
2761/// workspace to leave.
2762fn strip_line_comments(src: &str) -> String {
2763 src.lines()
2764 .map(|line| {
2765 let bytes = line.as_bytes();
2766 let (mut in_str, mut escaped) = (false, false);
2767 for (i, &b) in bytes.iter().enumerate() {
2768 if escaped {
2769 escaped = false;
2770 } else if b == b'\\' && in_str {
2771 escaped = true;
2772 } else if b == b'"' {
2773 in_str = !in_str;
2774 } else if b == b'/' && !in_str && bytes.get(i + 1) == Some(&b'/') {
2775 return &line[..i];
2776 }
2777 }
2778 line
2779 })
2780 .collect::<Vec<_>>()
2781 .join("\n")
2782}
2783
2784/// Every `bynk.<family>.<name>` token in `text` — the shape of a registry code,
2785/// whether quoted (Rust source) or bare (fixtures, transcripts).
2786fn registry_tokens(text: &str) -> Vec<String> {
2787 let bytes = text.as_bytes();
2788 let ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'.';
2789 let mut out = Vec::new();
2790 let mut i = 0;
2791 while let Some(rel) = text[i..].find("bynk.") {
2792 let start = i + rel;
2793 let mut end = start;
2794 while end < bytes.len() && ident(bytes[end]) {
2795 end += 1;
2796 }
2797 if start == 0 || !ident(bytes[start - 1]) {
2798 out.push(text[start..end].trim_end_matches('.').to_string());
2799 }
2800 i = end.max(start + 1);
2801 }
2802 out
2803}
2804
2805/// Every readable UTF-8 file under `dir`, recursively, as `(path, contents)`.
2806fn text_files(dir: &Path) -> Vec<(PathBuf, String)> {
2807 let mut out = Vec::new();
2808 let mut stack = vec![dir.to_path_buf()];
2809 while let Some(d) = stack.pop() {
2810 let Ok(entries) = std::fs::read_dir(&d) else {
2811 continue;
2812 };
2813 for entry in entries.flatten() {
2814 let path = entry.path();
2815 if path.is_dir() {
2816 stack.push(path);
2817 } else if let Ok(contents) = std::fs::read_to_string(&path) {
2818 out.push((path, contents));
2819 }
2820 }
2821 }
2822 out
2823}
2824
2825fn diagnostic_coverage(root: &Path) -> Probe {
2826 let total = bynk_syntax::diagnostics::REGISTRY.len();
2827 let unasserted = unasserted_codes(root).len();
2828 Probe {
2829 name: "diagnostic_coverage",
2830 gated: true,
2831 reads: format!(
2832 "unasserted={unasserted} (asserted {}/{total})",
2833 total - unasserted
2834 ),
2835 }
2836}
2837
2838// --- Reported probe 4: fixture_kinds --------------------------------------
2839
2840/// R11.2. Fixture directories under `bynkc/tests` using each assertion granularity —
2841/// `expected_contains.txt` / `expected_absent.txt` / `expected_diagnostics.txt` — set
2842/// against the older, coarser `expected_error.txt` (category-string) convention.
2843///
2844/// #1660 (runtime-semantics track §3.5) added two counts:
2845/// - `warnings` (`expected_warnings.txt`, a positive fixture's pinned warnings), which
2846/// this probe had never counted;
2847/// - `run` (`expected_run.txt`), the positive fixtures whose `suite`s
2848/// `bynkc/tests/behaviour_fixtures.rs` actually *runs*, the one granularity that
2849/// asserts runtime behaviour rather than emitted text or diagnostics.
2850fn fixture_kinds(root: &Path) -> Probe {
2851 let tests_dir = root.join("bynkc/tests");
2852 let contains = count_files_named(&tests_dir, "expected_contains.txt");
2853 let absent = count_files_named(&tests_dir, "expected_absent.txt");
2854 let diagnostics = count_files_named(&tests_dir, "expected_diagnostics.txt");
2855 let error = count_files_named(&tests_dir, "expected_error.txt");
2856 let warnings = count_files_named(&tests_dir, "expected_warnings.txt");
2857 let run = count_files_named(&tests_dir, "expected_run.txt");
2858 Probe {
2859 name: "fixture_kinds",
2860 gated: false,
2861 reads: format!(
2862 "contains={contains}, absent={absent}, diagnostics={diagnostics}, error={error}, warnings={warnings}, run={run}"
2863 ),
2864 }
2865}
2866
2867fn count_files_named(dir: &Path, filename: &str) -> usize {
2868 let mut count = 0usize;
2869 count_files_named_walk(dir, filename, &mut count);
2870 count
2871}
2872
2873fn count_files_named_walk(dir: &Path, filename: &str, count: &mut usize) {
2874 let Ok(entries) = std::fs::read_dir(dir) else {
2875 return;
2876 };
2877 for entry in entries.flatten() {
2878 let path = entry.path();
2879 if path.is_dir() {
2880 count_files_named_walk(&path, filename, count);
2881 } else if path.file_name().is_some_and(|n| n == filename) {
2882 *count += 1;
2883 }
2884 }
2885}
2886
2887// --- Reported probe 5: keystroke_latency ------------------------------------
2888
2889/// Phase 8's own trend-only probe (`design/bynk-compiler-trajectory.md` §3,
2890/// "keystroke-to-diagnostic latency by query level") — settled (Q3/Q5, ADR 0414;
2891/// `design/tracks/incrementality.md` §5) as staying **"not measured" for this whole
2892/// phase's lifetime**: R3.15's scheduler decision defers whole (no memo table, salsa or
2893/// otherwise, ships in phase 8), and the literal latency number presupposes query
2894/// levels attributing latency to — levels [`incremental_query_types`] itself proves
2895/// exist, but attributing real latency to them needs a scheduler this phase
2896/// deliberately does not build. Added now, not deferred to whenever a scheduler
2897/// exists, so the trajectory's own §3.0 baseline table carries a live, CI-computed row
2898/// instead of a static doc claim — the same "instrument even a number that won't move
2899/// yet" precedent `test_density`/`fixture_kinds` already set for this module.
2900fn keystroke_latency(_root: &Path) -> Probe {
2901 Probe {
2902 name: "keystroke_latency",
2903 gated: false,
2904 reads: "not measured — no scheduler exists yet (R3.15, deferred whole this phase)"
2905 .to_string(),
2906 }
2907}
2908
2909// --- Rendering + diffing ---------------------------------------------------
2910
2911/// The committed table: a plain Markdown table, probe name → gated?/reads, plus a
2912/// pointer to the rule ledger `stamp::apply` writes (#1001).
2913pub fn render_table(report: &Report) -> String {
2914 let mut out = String::new();
2915 out.push_str("<!-- GENERATED FILE — do not edit by hand.\n");
2916 out.push_str(" Source: cargo xtask greenfield-status (xtask/src/greenfield_status.rs).\n");
2917 out.push_str(" Regenerate with: cargo xtask greenfield-status --apply -->\n\n");
2918 out.push_str("# Greenfield status\n\n");
2919 out.push_str(
2920 "Track slice T0.0 (#999); `ts_writes`/`ts_any` added by P7.0 (#1296); \
2921 `verbatim_origins`/`verbatim_sites` added by P7.5 (#1307); \
2922 `incremental_query_types`/`keystroke_latency` added by P8.0 (#1510); \
2923 `unconsumed_ir_items` added by Slice D3 of the IR cutover (#1542); \
2924 `diagnostic_coverage` added by #1662. Sixteen \
2925 probes are gated — a disagreement between this file and a fresh run fails \
2926 `greenfield_status_table_is_current` (`xtask/tests/greenfield_status.rs`). \
2927 Five are trend probes, reported only.\n\n",
2928 );
2929 out.push_str("| Probe | Gated | Reads |\n|---|---|---|\n");
2930 for probe in &report.probes {
2931 let _ = writeln!(
2932 out,
2933 "| `{}` | {} | {} |",
2934 probe.name,
2935 if probe.gated { "yes" } else { "no (trend)" },
2936 probe.reads
2937 );
2938 }
2939
2940 out.push_str("\n## Rules closed\n\n");
2941 // A static, unconditional link — not a count, and not even an existence
2942 // check. A first draft read `design/greenfield-status-rules.md` here to
2943 // report a row count, but nothing regenerates *this* file when `stamp`
2944 // writes the ledger (`stamp.yml` never runs `greenfield-status --apply`,
2945 // and the gating test only diffs the nine probes) — so a count or an
2946 // exists/doesn't-exist message would silently go stale the moment the
2947 // first `closes_rule` landed, which is exactly the drift this section
2948 // exists to avoid, not invite (#1001 review). Static text can't go stale;
2949 // the ledger is one click away either way.
2950 out.push_str(
2951 "See [`design/greenfield-status-rules.md`](greenfield-status-rules.md) for rule ids \
2952 closed so far (written by `cargo xtask stamp --apply` at merge; may not exist yet if \
2953 no increment has cited `closes_rule`).\n",
2954 );
2955 out
2956}
2957
2958/// Every gated probe whose live reading disagrees with the committed table's, as
2959/// `(probe name, committed, live)`. Trend probes are never compared, and never
2960/// computed here — this only runs the sixteen gated probes, so checking currency never
2961/// pays for `wildcard_arms`'s workspace-wide clippy pass. For a caller that has already
2962/// run the full report (e.g. to print it), use [`gated_disagreements_in`] instead so the
2963/// sixteen gated probes aren't computed a second time.
2964pub fn gated_disagreements(root: &Path) -> Vec<(String, String, String)> {
2965 gated_disagreements_in(&run_gated(root), root)
2966}
2967
2968/// Like [`gated_disagreements`], but diffs `probes` (typically a [`Report`]'s
2969/// `.probes`, already computed) instead of re-running the gated probes.
2970pub fn gated_disagreements_in(probes: &[Probe], root: &Path) -> Vec<(String, String, String)> {
2971 let committed = std::fs::read_to_string(table_path(root)).unwrap_or_default();
2972 let mut out = Vec::new();
2973 for probe in probes.iter().filter(|p| p.gated) {
2974 let row_prefix = format!("| `{}` | yes | ", probe.name);
2975 let committed_reads = committed
2976 .lines()
2977 .find(|l| l.starts_with(&row_prefix))
2978 .and_then(|l| l.strip_prefix(&row_prefix))
2979 .and_then(|l| l.strip_suffix(" |"))
2980 .unwrap_or("<row missing>");
2981 if committed_reads != probe.reads {
2982 out.push((
2983 probe.name.to_string(),
2984 committed_reads.to_string(),
2985 probe.reads.clone(),
2986 ));
2987 }
2988 }
2989 out
2990}
2991
2992/// The disagreements no pending file acknowledges — what actually fails the gate.
2993///
2994/// A gated probe may move in a PR in one of two ways: the PR regenerates
2995/// `design/greenfield-status.md` itself (the original mechanism, still valid for
2996/// a change that carries no increment), or the PR's `design/pending/<slug>.md`
2997/// names the probe in its `probes` key and leaves the table alone. The second
2998/// keeps the acknowledgement in review — a probe still cannot move silently —
2999/// without every probe-moving PR editing the same shared rows, which is what
3000/// made this table the repo's most-conflicted file. The stamp then rewrites the
3001/// gated rows on `main` in merge order ([`refresh_gated_rows`]).
3002pub fn unacknowledged(
3003 disagreements: Vec<(String, String, String)>,
3004 acknowledged: &BTreeSet<String>,
3005) -> Vec<(String, String, String)> {
3006 disagreements
3007 .into_iter()
3008 .filter(|(name, _, _)| !acknowledged.contains(name))
3009 .collect()
3010}
3011
3012/// `committed` (the table's current text) with every gated row's reading
3013/// replaced by `probes`' live one; trend rows, prose and the rule-ledger pointer
3014/// are left exactly as committed, so the stamp never pays for the trend probes'
3015/// workspace-wide clippy pass. Errors naming any gated probe with no row to
3016/// rewrite — a table that has lost a row needs a full
3017/// `cargo xtask greenfield-status --apply`, not a silent append.
3018pub fn refresh_gated_rows(committed: &str, probes: &[Probe]) -> Result<String, String> {
3019 let mut out = String::with_capacity(committed.len());
3020 let mut seen = BTreeSet::new();
3021 for line in committed.split_inclusive('\n') {
3022 let replacement = probes.iter().filter(|p| p.gated).find_map(|p| {
3023 let prefix = format!("| `{}` | yes | ", p.name);
3024 line.starts_with(&prefix).then(|| {
3025 seen.insert(p.name);
3026 let eol = if line.ends_with('\n') { "\n" } else { "" };
3027 format!("{prefix}{} |{eol}", p.reads)
3028 })
3029 });
3030 out.push_str(replacement.as_deref().unwrap_or(line));
3031 }
3032 let missing: Vec<_> = probes
3033 .iter()
3034 .filter(|p| p.gated && !seen.contains(p.name))
3035 .map(|p| p.name)
3036 .collect();
3037 if missing.is_empty() {
3038 Ok(out)
3039 } else {
3040 Err(format!(
3041 "design/greenfield-status.md has no row for gated probe(s) {} — run \
3042 `cargo xtask greenfield-status --apply`",
3043 missing.join(", ")
3044 ))
3045 }
3046}
3047
3048/// The stamp's refreshed table: [`refresh_gated_rows`], but only if every gated
3049/// probe whose live reading differs from `committed` is named by one of the
3050/// increments being stamped (`acknowledged` — the union of their `probes`).
3051///
3052/// Rewriting every gated row unconditionally would silently absorb a move that
3053/// reached `main` with no acknowledgement (a bypass push, say): after the stamp
3054/// the table would match by construction, and nothing downstream could notice.
3055/// Refusing instead keeps the invariant the gate enforces on PRs — a gated probe
3056/// never moves unacknowledged — true on `main` too. The stamp computes this
3057/// before applying, so a refusal leaves nothing written and the pending files
3058/// in place.
3059pub fn refresh_for_stamp(
3060 committed: &str,
3061 probes: &[Probe],
3062 acknowledged: &BTreeSet<String>,
3063) -> Result<String, String> {
3064 let unnamed: Vec<String> = probes
3065 .iter()
3066 .filter(|p| p.gated && !acknowledged.contains(p.name))
3067 .filter_map(|p| {
3068 let prefix = format!("| `{}` | yes | ", p.name);
3069 let committed_reads = committed
3070 .lines()
3071 .find(|l| l.starts_with(&prefix))?
3072 .strip_prefix(&prefix)?
3073 .strip_suffix(" |")?;
3074 (committed_reads != p.reads).then(|| {
3075 format!(
3076 " {}: committed {committed_reads:?}, live {:?}",
3077 p.name, p.reads
3078 )
3079 })
3080 })
3081 .collect();
3082 if !unnamed.is_empty() {
3083 return Err(format!(
3084 "gated greenfield-status probe(s) moved without being named by any increment being \
3085 stamped — refusing to absorb them into the table. Acknowledge them in a pending \
3086 file or regenerate the table in a PR:\n{}",
3087 unnamed.join("\n")
3088 ));
3089 }
3090 refresh_gated_rows(committed, probes)
3091}
3092
3093#[cfg(test)]
3094mod tests {
3095 use super::*;
3096
3097 // --- probe acknowledgement + refresh ------------------------------------
3098
3099 fn gated(name: &'static str, reads: &str) -> Probe {
3100 Probe {
3101 name,
3102 gated: true,
3103 reads: reads.to_string(),
3104 }
3105 }
3106
3107 #[test]
3108 fn unacknowledged_drops_only_acknowledged_probes() {
3109 let d = vec![
3110 (
3111 "ts_writes".to_string(),
3112 "850".to_string(),
3113 "851".to_string(),
3114 ),
3115 ("ts_any".to_string(), "26".to_string(), "27".to_string()),
3116 ];
3117 let ack: BTreeSet<String> = ["ts_writes".to_string()].into();
3118 let left = unacknowledged(d, &ack);
3119 assert_eq!(left.len(), 1);
3120 assert_eq!(left[0].0, "ts_any");
3121 }
3122
3123 #[test]
3124 fn refresh_rewrites_gated_rows_and_leaves_the_rest() {
3125 let committed = "# Greenfield status\n\n| Probe | Gated | Reads |\n|---|---|---|\n\
3126 | `ts_writes` | yes | 850 |\n\
3127 | `wildcard_arms` | no (trend) | 355 |\n\nfooter\n";
3128 let out = refresh_gated_rows(committed, &[gated("ts_writes", "851")]).unwrap();
3129 assert!(out.contains("| `ts_writes` | yes | 851 |\n"));
3130 assert!(out.contains("| `wildcard_arms` | no (trend) | 355 |\n"));
3131 assert!(out.ends_with("\nfooter\n"));
3132 assert_eq!(out.lines().count(), committed.lines().count());
3133 }
3134
3135 const STAMP_TABLE: &str = "| Probe | Gated | Reads |\n|---|---|---|\n\
3136 | `ts_writes` | yes | 850 |\n\
3137 | `ts_any` | yes | 26 |\n";
3138
3139 #[test]
3140 fn stamp_refresh_rewrites_named_moves() {
3141 let ack: BTreeSet<String> = ["ts_writes".to_string()].into();
3142 let out = refresh_for_stamp(
3143 STAMP_TABLE,
3144 &[gated("ts_writes", "851"), gated("ts_any", "26")],
3145 &ack,
3146 )
3147 .unwrap();
3148 assert!(out.contains("| `ts_writes` | yes | 851 |"));
3149 assert!(out.contains("| `ts_any` | yes | 26 |"));
3150 }
3151
3152 #[test]
3153 fn stamp_refresh_refuses_an_unnamed_move() {
3154 let ack: BTreeSet<String> = ["ts_writes".to_string()].into();
3155 let err = refresh_for_stamp(
3156 STAMP_TABLE,
3157 &[gated("ts_writes", "851"), gated("ts_any", "27")],
3158 &ack,
3159 )
3160 .unwrap_err();
3161 assert!(err.contains("ts_any"), "{err}");
3162 assert!(!err.contains("ts_writes"), "{err}");
3163 }
3164
3165 #[test]
3166 fn refresh_errors_on_a_missing_gated_row() {
3167 let err = refresh_gated_rows("| Probe | Gated | Reads |\n", &[gated("ts_any", "26")])
3168 .unwrap_err();
3169 assert!(err.contains("ts_any"));
3170 }
3171
3172 // --- emit_diagnostics (#999 Decision A) ---------------------------------
3173
3174 /// A standalone `"bynk.foo"` literal is found — the ordinary case.
3175 #[test]
3176 fn bynk_dotted_literals_finds_standalone_literal() {
3177 let src = r#"code("bynk.check.something", "a message")"#;
3178 assert_eq!(bynk_dotted_literals(src), vec!["bynk.check.something"]);
3179 }
3180
3181 /// The bug this slice found in its own first draft: a longer message that merely
3182 /// *starts* with "bynk." must not be truncated into a fake code literal. Regression
3183 /// test for `bynk.map itself uses bynk.list, so list must be injected too: {paths:?}`
3184 /// (`bynk-emit/src/project.rs`), which an earlier, less careful version of this scan
3185 /// wrongly counted as the literal `"bynk.map"`.
3186 #[test]
3187 fn bynk_dotted_literals_ignores_prefix_of_a_longer_message() {
3188 let src = r#"assert!(cond, "bynk.map itself uses bynk.list, so list must be injected too: {paths:?}");"#;
3189 assert!(bynk_dotted_literals(src).is_empty());
3190 }
3191
3192 /// Regression test for the other half of the same bug: a `\`-continued string
3193 /// literal (`"bynk.emit.unresolved_cross_context_signature: no signature for \`,
3194 /// continued on the next source line) is one string, not a diagnostic-code literal,
3195 /// even though its first segment matches the identifier charset — because the
3196 /// character after the run is `:`, never a closing quote, on either line.
3197 #[test]
3198 fn bynk_dotted_literals_ignores_a_line_continued_message() {
3199 let src =
3200 "\"bynk.emit.unresolved_cross_context_signature: no signature for \\\n the rest\"";
3201 assert!(bynk_dotted_literals(src).is_empty());
3202 }
3203
3204 /// The whole point of Decision A: cross-referencing the real registry, not a
3205 /// hand-maintained exclusion list, correctly separates a real diagnostic code from
3206 /// a commons/namespace path that merely looks like one.
3207 #[test]
3208 fn emit_diagnostics_cross_references_the_real_registry() {
3209 let registry: BTreeSet<&str> = bynk_syntax::diagnostics::REGISTRY
3210 .iter()
3211 .map(|d| d.code)
3212 .collect();
3213 // A code this registry is known to carry (bynk-syntax/src/diagnostics.rs).
3214 assert!(registry.contains("bynk.parse.expected_expression"));
3215 // A commons/namespace path, not a diagnostic code — #999's own verified survey.
3216 assert!(!registry.contains("bynk.locale"));
3217 }
3218
3219 // --- ast_importers (#1176) ------------------------------------------------
3220
3221 /// The exclusion is named, not prefixed: `project/tests_emit.rs` is the
3222 /// Q7-settled `Ir → String` half that keeps hand-writing TypeScript by calling
3223 /// straight into `emitter.rs`'s own body-rendering, and
3224 /// keeps reading a handler's declared param/return `TypeRef` with no `TyId`
3225 /// available at that call site — but `project.rs` (which also imports
3226 /// `bynk_syntax::ast`, via `EmitProjectCtx`) must stay counted, and so, per
3227 /// review of #1210, must `emitter.rs`/`emitter/lower.rs` themselves: both still
3228 /// hold live AST-*declaration* reads (`emitter.rs`'s `CommonsItem::Service`/
3229 /// `svc.protocol` walk, `emitter/lower.rs`'s `cap_op_param_names`) that are the
3230 /// still-open R6.13 defect this probe tracks, not the Q7 kind — excluding either
3231 /// file would hide that real work the same way a path-prefix rule would. A
3232 /// path-prefix rule (e.g. "only `emitter/**` counts") would have excluded
3233 /// `project.rs` right along with the legitimate ones, silently undercounting
3234 /// real work. (`ir.rs`/`ir/lower.rs`, the lowering pass's own former `Ast → Ir`
3235 /// exclusion, left this list at Arc D's P7.12 crate carve — they left
3236 /// `bynk-emit/src` entirely, not merely this list.)
3237 #[test]
3238 fn ast_importer_exclusion_is_named_not_prefixed() {
3239 assert!(is_named_ast_importer(Path::new("project/tests_emit.rs")));
3240 assert!(is_named_ast_importer(Path::new("emitter/serialisation.rs")));
3241 assert!(!is_named_ast_importer(Path::new("project.rs")));
3242 assert!(!is_named_ast_importer(Path::new("emitter.rs")));
3243 assert!(!is_named_ast_importer(Path::new("emitter/lower.rs")));
3244 assert!(!is_named_ast_importer(Path::new("emitter/workers.rs")));
3245 assert!(!is_named_ast_importer(Path::new("ir.rs")));
3246 assert!(!is_named_ast_importer(Path::new("ir/lower.rs")));
3247 }
3248
3249 /// #1184 review: an `AST_IMPORTER_EXCEPTIONS` entry going stale (renamed or split,
3250 /// e.g. `ir/lower.rs` becoming `ir/lower/mod.rs`) must fail loud here, not surface
3251 /// as a silent `ast_importers` regression in `greenfield_status_table_is_current` —
3252 /// mirrors [`file_is_named_fs_floor`]'s own "fail loud, not quiet" discipline.
3253 #[test]
3254 fn ast_importer_exceptions_still_exist_and_still_import_the_ast() {
3255 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3256 .join("..")
3257 .join("bynk-emit/src");
3258 for rel in AST_IMPORTER_EXCEPTIONS {
3259 let contents = std::fs::read_to_string(dir.join(rel)).unwrap_or_else(|e| {
3260 panic!("AST_IMPORTER_EXCEPTIONS entry {rel:?} does not exist: {e}")
3261 });
3262 assert!(
3263 contents.contains("bynk_syntax::ast"),
3264 "AST_IMPORTER_EXCEPTIONS entry {rel:?} no longer imports bynk_syntax::ast \
3265 — it excludes nothing and should be removed"
3266 );
3267 }
3268 }
3269
3270 /// #1184 review, extended by #1187's own closing scoping pass (and narrowed by
3271 /// review of #1210, which found `emitter.rs`/`emitter/lower.rs` still hold live,
3272 /// in-scope AST-declaration reads and must stay counted) and by P6.33's own
3273 /// re-settling (`emitter/serialisation.rs`, a phase boundary rather than a
3274 /// declaration-read exemption): exercises the real filter over the live tree, not
3275 /// just the pure predicate — the survivor set the PR's own named-vs-prefix
3276 /// argument depends on: the named exclusions drop out
3277 /// (`project/tests_emit.rs`'s Q7-settled `Ir → String` case and
3278 /// `emitter/serialisation.rs`'s phase-7 codec renderer), while `emitter.rs`/
3279 /// `emitter/lower.rs`/`emitter/workers.rs` do not. `ir.rs`/`ir/lower.rs` (the
3280 /// lowering pass's own former `Ast → Ir` pair, excluded here until Arc D's
3281 /// P7.12 crate carve) are asserted absent below for a different reason now:
3282 /// they left `bynk-emit/src` entirely, so `ast_importer_files` never walks
3283 /// them at all, named exclusion or not.
3284 ///
3285 /// P6.49 (phase 6's own §6b): `project.rs` and `project/diagnostics.rs`
3286 /// join the *excluded* side of this assertion — the opposite of what this test
3287 /// checked before. `project.rs` cleared without joining
3288 /// [`AST_IMPORTER_EXCEPTIONS`]: nine slices (P6.42–P6.49) either relocated its
3289 /// remaining declaration reads to the `bynk-check`/`bynk-project` crates that
3290 /// already own the data, or re-exported a type from a `bynk-check` module whose
3291 /// own public API was already parameterised by it (the P6.27 `ExprId` precedent,
3292 /// applied to `TypeDecl`/`FnDecl`/`Visibility`/`ActorDecl`) — real, verified
3293 /// movement, not a probe exemption. `project/diagnostics.rs` rides on it, per the
3294 /// same super-glob rule this file's own regression guard below pins.
3295 #[test]
3296 fn ast_importers_excludes_the_named_pairs_and_project_rs() {
3297 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
3298 let dir = root.join("bynk-emit/src");
3299 let counted: BTreeSet<String> = ast_importer_files(&root)
3300 .into_iter()
3301 .map(|path| {
3302 path.strip_prefix(&dir)
3303 .unwrap_or(&path)
3304 .to_string_lossy()
3305 .replace('\\', "/")
3306 })
3307 .collect();
3308 assert!(!counted.contains("ir.rs"), "moved to bynk-ir at P7.12");
3309 assert!(
3310 !counted.contains("ir/lower.rs"),
3311 "moved to bynk-lower at P7.12"
3312 );
3313 assert!(!counted.contains("project/tests_emit.rs"));
3314 assert!(!counted.contains("emitter/serialisation.rs"));
3315 assert!(!counted.contains("project.rs"));
3316 assert!(!counted.contains("project/diagnostics.rs"));
3317 assert!(counted.contains("emitter.rs"));
3318 assert!(counted.contains("emitter/lower.rs"));
3319 assert!(counted.contains("emitter/workers.rs"));
3320 }
3321
3322 /// P6.26 review (#1259): a module-level `use super::*;` is a real inheritance
3323 /// channel (Rust's own privacy rule makes a parent's private `use` visible to
3324 /// descendants) — must be detected — but a `use super::*;` nested inside a
3325 /// `#[cfg(test)] mod tests { .. }` block glob-imports its own *immediately
3326 /// enclosing* module, not the grandparent file on disk, and must not
3327 /// false-positive.
3328 #[test]
3329 fn module_level_super_glob_detection_ignores_nested_test_mod() {
3330 assert!(has_module_level_super_glob(
3331 "use std::fmt;\nuse super::*;\n"
3332 ));
3333 assert!(!has_module_level_super_glob(
3334 "fn f() {}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n}\n"
3335 ));
3336 }
3337
3338 /// P6.26 review (#1259): pins the real scenario the review found —
3339 /// `emitter/emit.rs` and `emitter/lower.rs` both carry a live, module-level
3340 /// `use super::*;` inheriting from `emitter.rs`, which itself still imports
3341 /// `bynk_syntax::ast` directly. Regression guard: if a future slice deletes
3342 /// either child's own explicit AST import while this inheritance channel and
3343 /// the parent's own AST dependency both remain, [`ast_importer_files`] must
3344 /// keep counting it rather than silently dropping the probe.
3345 #[test]
3346 fn super_glob_children_of_an_ast_importing_parent_are_detected() {
3347 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
3348 let dir = root.join("bynk-emit/src");
3349 for rel in ["emitter/emit.rs", "emitter/lower.rs"] {
3350 let contents = std::fs::read_to_string(dir.join(rel))
3351 .unwrap_or_else(|e| panic!("{rel:?} does not exist: {e}"));
3352 assert!(
3353 has_module_level_super_glob(&contents),
3354 "{rel:?} no longer carries a module-level `use super::*;` — this \
3355 regression guard (and the false-zero hazard it pins) no longer applies \
3356 and may be deleted"
3357 );
3358 assert_eq!(
3359 super_glob_parent_imports_ast(&dir, Path::new(rel)),
3360 Some(true),
3361 "{rel:?}'s parent (`emitter.rs`) no longer imports bynk_syntax::ast — \
3362 update this guard's expectation"
3363 );
3364 }
3365 // A file directly under `bynk-emit/src` (no directory component) has no
3366 // `use super::*;` parent to inherit from.
3367 assert_eq!(
3368 super_glob_parent_imports_ast(&dir, Path::new("emitter.rs")),
3369 None
3370 );
3371 }
3372
3373 // --- fs_below_driver / test_density (trailing `#[cfg(test)] mod tests {}`) ---
3374
3375 #[test]
3376 fn production_std_fs_usage_is_detected() {
3377 let src = "fn load(p: &Path) -> String {\n std::fs::read_to_string(p).unwrap()\n}\n";
3378 assert!(has_production_std_fs(src));
3379 }
3380
3381 #[test]
3382 fn std_fs_inside_a_trailing_test_mod_is_not_production() {
3383 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";
3384 assert!(!has_production_std_fs(src));
3385 }
3386
3387 /// Regression test for the other real bug this slice found: `bynk-emit/src/lib.rs`
3388 /// has `#[cfg(test)] pub(crate) mod testkit;` — an external-file module
3389 /// *declaration* (ends in `;`), not an inline block. It must not be mistaken for a
3390 /// scope-opening `mod tests { ... }`, or the (genuinely production) code after it in
3391 /// the same file would be wrongly excluded.
3392 #[test]
3393 fn cfg_test_external_mod_declaration_does_not_open_a_test_region() {
3394 let src = "#[cfg(test)]\npub(crate) mod testkit;\n\nfn load(p: &Path) -> String {\n std::fs::read_to_string(p).unwrap()\n}\n";
3395 assert!(has_production_std_fs(src));
3396 }
3397
3398 // --- unconsumed_ir_items (Slice D3 of #1542) ----------------------------
3399
3400 fn files(entries: &[(&str, &str, &str)]) -> Vec<(String, PathBuf, String)> {
3401 entries
3402 .iter()
3403 .map(|(k, p, c)| (k.to_string(), PathBuf::from(p), c.to_string()))
3404 .collect()
3405 }
3406
3407 /// The ordinary case: a `pub` item read from another crate's production
3408 /// code is consumed; one read from nowhere is not.
3409 #[test]
3410 fn unconsumed_pub_items_reports_only_items_with_no_other_crate_reader() {
3411 let owners = files(&[(
3412 "bynk-ir",
3413 "lib.rs",
3414 "pub struct Used;\npub struct Unused;\npub fn helper() {}\n",
3415 )]);
3416 let consumers = files(&[
3417 (
3418 "bynk-ir",
3419 "lib.rs",
3420 "pub struct Used;\npub struct Unused;\npub fn helper() {}\n",
3421 ),
3422 (
3423 "bynk-emit",
3424 "a.rs",
3425 "fn f(x: bynk_ir::Used) { helper(); }\n",
3426 ),
3427 ]);
3428 assert_eq!(
3429 unconsumed_pub_items(&["bynk-ir"], &owners, &consumers),
3430 vec!["bynk-ir::Unused"]
3431 );
3432 }
3433
3434 /// A mention in a comment, inside a `#[cfg(test)]` module, or in the
3435 /// owning crate itself is not a consumer.
3436 #[test]
3437 fn unconsumed_pub_items_ignores_comments_tests_and_the_owning_crate() {
3438 let owners = files(&[(
3439 "bynk-lower",
3440 "lib.rs",
3441 "pub fn lower_x() {}\npub fn lower_y() {}\npub fn lower_z() {}\n",
3442 )]);
3443 let consumers = files(&[
3444 (
3445 "bynk-lower",
3446 "lib.rs",
3447 "pub fn lower_x() {}\nfn own() { lower_x(); }\n",
3448 ),
3449 (
3450 "bynk-emit",
3451 "a.rs",
3452 "// lower_y() used to be called here\n/// and [`lower_y`] linked here\n",
3453 ),
3454 (
3455 "bynk-emit",
3456 "b.rs",
3457 "fn prod() {}\n\n#[cfg(test)]\nmod tests {\n fn t() { lower_z(); }\n}\n",
3458 ),
3459 ]);
3460 assert_eq!(
3461 unconsumed_pub_items(&["bynk-lower"], &owners, &consumers),
3462 vec![
3463 "bynk-lower::lower_x",
3464 "bynk-lower::lower_y",
3465 "bynk-lower::lower_z"
3466 ]
3467 );
3468 }
3469
3470 /// Whole-word matching: `IrExpr` inside `IrExprKind` is not a read of
3471 /// `IrExpr`, and `pub(crate)`/indented items are not offered to other
3472 /// crates so are never counted either way.
3473 #[test]
3474 fn unconsumed_pub_items_matches_whole_words_and_skips_non_public_items() {
3475 let owners = files(&[(
3476 "bynk-ir",
3477 "lib.rs",
3478 "pub struct IrExpr;\npub enum IrExprKind {}\npub(crate) fn internal() {}\nimpl IrExpr {\n pub fn method() {}\n}\n",
3479 )]);
3480 let consumers = files(&[("bynk-emit", "a.rs", "fn f(k: IrExprKind) {}\n")]);
3481 assert_eq!(
3482 unconsumed_pub_items(&["bynk-ir"], &owners, &consumers),
3483 vec!["bynk-ir::IrExpr"]
3484 );
3485 }
3486
3487 /// The two IR crates do not vouch for each other (review of #1581): a
3488 /// `bynk-ir` type read only from `bynk-lower` is unconsumed, and so is a
3489 /// `bynk-lower` helper read only from `bynk-ir` — pre-D0 `main`'s exact
3490 /// shape, where `bynk-lower`'s own unconsumed constructors named every
3491 /// unconsumed `bynk-ir` type.
3492 #[test]
3493 fn unconsumed_pub_items_does_not_let_owner_crates_vouch_for_each_other() {
3494 let owners = files(&[
3495 (
3496 "bynk-ir",
3497 "lib.rs",
3498 "pub struct IrExpr;\npub struct Shape;\n",
3499 ),
3500 (
3501 "bynk-lower",
3502 "lib.rs",
3503 "pub fn lower_expr_ir() -> IrExpr { IrExpr }\npub fn shape() -> Shape { Shape }\n",
3504 ),
3505 ]);
3506 let consumers = files(&[
3507 (
3508 "bynk-ir",
3509 "lib.rs",
3510 "pub struct IrExpr;\npub struct Shape;\n",
3511 ),
3512 (
3513 "bynk-lower",
3514 "lib.rs",
3515 "pub fn lower_expr_ir() -> IrExpr { IrExpr }\npub fn shape() -> Shape { Shape }\n",
3516 ),
3517 (
3518 "bynk-emit",
3519 "a.rs",
3520 "fn f() -> Shape { bynk_lower::shape() }\n",
3521 ),
3522 ]);
3523 assert_eq!(
3524 unconsumed_pub_items(&["bynk-ir", "bynk-lower"], &owners, &consumers),
3525 vec!["bynk-ir::IrExpr", "bynk-lower::lower_expr_ir"]
3526 );
3527 }
3528
3529 /// One item declared in two files of the same crate (a split module) is
3530 /// reported once, not twice.
3531 #[test]
3532 fn unconsumed_pub_items_deduplicates_a_name_declared_in_two_files() {
3533 let owners = files(&[
3534 ("bynk-lower", "a.rs", "pub fn twice() {}\n"),
3535 ("bynk-lower", "b.rs", "pub fn twice() {}\n"),
3536 ]);
3537 assert_eq!(
3538 unconsumed_pub_items(&["bynk-lower"], &owners, &[]),
3539 vec!["bynk-lower::twice"]
3540 );
3541 }
3542
3543 #[test]
3544 fn column_zero_pub_item_name_accepts_every_item_kind_and_fn_qualifier() {
3545 assert_eq!(column_zero_pub_item_name("pub fn f(x: i32) {}"), Some("f"));
3546 assert_eq!(column_zero_pub_item_name("pub struct S<'a> {"), Some("S"));
3547 assert_eq!(column_zero_pub_item_name("pub enum E {"), Some("E"));
3548 assert_eq!(
3549 column_zero_pub_item_name("pub type T = (u8, u8);"),
3550 Some("T")
3551 );
3552 assert_eq!(
3553 column_zero_pub_item_name("pub const C: &[&str] = &[];"),
3554 Some("C")
3555 );
3556 assert_eq!(
3557 column_zero_pub_item_name("pub static S: &[&str] = &[];"),
3558 Some("S")
3559 );
3560 assert_eq!(column_zero_pub_item_name("pub trait Tr {}"), Some("Tr"));
3561 assert_eq!(column_zero_pub_item_name("pub union U {"), Some("U"));
3562 // `fn` qualifiers, alone and stacked (review of #1581).
3563 assert_eq!(column_zero_pub_item_name("pub async fn a() {}"), Some("a"));
3564 assert_eq!(column_zero_pub_item_name("pub unsafe fn u() {}"), Some("u"));
3565 assert_eq!(column_zero_pub_item_name("pub const fn c() {}"), Some("c"));
3566 assert_eq!(
3567 column_zero_pub_item_name("pub extern \"C\" fn x() {}"),
3568 Some("x")
3569 );
3570 assert_eq!(
3571 column_zero_pub_item_name("pub const unsafe fn cu() {}"),
3572 Some("cu")
3573 );
3574 assert_eq!(
3575 column_zero_pub_item_name("pub unsafe extern \"C\" fn ue() {}"),
3576 Some("ue")
3577 );
3578 assert_eq!(
3579 column_zero_pub_item_name("pub async unsafe fn au() {}"),
3580 Some("au")
3581 );
3582 // Not items offered to another crate.
3583 assert_eq!(column_zero_pub_item_name("pub(crate) fn g() {}"), None);
3584 assert_eq!(column_zero_pub_item_name("pub(super) struct P;"), None);
3585 assert_eq!(column_zero_pub_item_name(" pub fn method() {}"), None);
3586 assert_eq!(column_zero_pub_item_name("pub use foo::Bar;"), None);
3587 assert_eq!(column_zero_pub_item_name("pub mod m;"), None);
3588 assert_eq!(column_zero_pub_item_name("pub impl Foo {}"), None);
3589 }
3590
3591 /// [`workspace_crate_src_files`] tags each file with its crate directory
3592 /// and only walks directories that are actually crates (a `Cargo.toml`
3593 /// *and* a `src/`), so a stray directory with one but not the other is
3594 /// neither an owner nor a consumer.
3595 #[test]
3596 fn workspace_crate_src_files_tags_files_by_crate_and_skips_non_crates() {
3597 let root = std::env::temp_dir().join(format!(
3598 "bynk-xtask-unconsumed-{}-{}",
3599 std::process::id(),
3600 std::time::SystemTime::now()
3601 .duration_since(std::time::UNIX_EPOCH)
3602 .map(|d| d.as_nanos())
3603 .unwrap_or(0)
3604 ));
3605 let mk = |rel: &str, contents: &str| {
3606 let p = root.join(rel);
3607 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
3608 std::fs::write(p, contents).unwrap();
3609 };
3610 mk("alpha/Cargo.toml", "[package]\nname = \"alpha\"\n");
3611 mk("alpha/src/lib.rs", "pub fn a() {}\n");
3612 mk("alpha/src/inner/mod.rs", "pub fn b() {}\n");
3613 mk("beta/Cargo.toml", "[package]\nname = \"beta\"\n");
3614 mk("beta/src/lib.rs", "fn c() { alpha::a() }\n");
3615 mk("no-src/Cargo.toml", "[package]\nname = \"no-src\"\n");
3616 mk("no-manifest/src/lib.rs", "pub fn d() {}\n");
3617 let files = workspace_crate_src_files(&root);
3618 let _ = std::fs::remove_dir_all(&root);
3619 let mut tagged: Vec<(String, String)> = files
3620 .iter()
3621 .map(|(k, p, _)| (k.clone(), p.to_string_lossy().replace('\\', "/")))
3622 .collect();
3623 tagged.sort();
3624 assert_eq!(
3625 tagged,
3626 vec![
3627 ("alpha".to_string(), "inner/mod.rs".to_string()),
3628 ("alpha".to_string(), "lib.rs".to_string()),
3629 ("beta".to_string(), "lib.rs".to_string()),
3630 ]
3631 );
3632 }
3633
3634 /// Regression test for the bug caught in review: a file with **two** scattered
3635 /// `#[cfg(test)] mod ... { ... }` blocks, with real production code between them —
3636 /// exactly `bynk-emit/src/emitter/lower.rs`'s shape (two test modules, 1031
3637 /// production lines apart). A single "everything from the first/last `#[cfg(test)]`
3638 /// onward" cutoff would misclassify `lower_lambda` here as test-scope; the fix must
3639 /// close each block at its own boundary and resume production scanning after it.
3640 #[test]
3641 fn production_code_between_two_scattered_test_mods_is_detected() {
3642 let src = "\
3643#[cfg(test)]
3644mod decode_map_key_tests {
3645 #[test]
3646 fn t() {
3647 assert_eq!(1, 1);
3648 }
3649}
3650
3651fn lower_lambda(p: &Path) -> String {
3652 std::fs::read_to_string(p).unwrap()
3653}
3654
3655#[cfg(test)]
3656mod idempotency_scoping_tests {
3657 #[test]
3658 fn t2() {
3659 assert_eq!(2, 2);
3660 }
3661}
3662";
3663 assert!(has_production_std_fs(src));
3664 }
3665
3666 /// The same fixture's `test_mod_ranges` shape, checked directly: two disjoint
3667 /// ranges, not one span from the first block to the last.
3668 #[test]
3669 fn test_mod_ranges_finds_each_block_separately() {
3670 let src = "\
3671#[cfg(test)]
3672mod a {
3673 fn x() {}
3674}
3675
3676fn production() {}
3677
3678#[cfg(test)]
3679mod b {
3680 fn y() {}
3681}
3682";
3683 let lines: Vec<&str> = src.lines().collect();
3684 let ranges = test_mod_ranges(&lines);
3685 assert_eq!(
3686 ranges.len(),
3687 2,
3688 "expected two disjoint test-mod ranges: {ranges:?}"
3689 );
3690 // Line 5 (0-indexed) is `fn production() {}`, between the two blocks.
3691 assert!(
3692 !in_test_range(5, &ranges),
3693 "production() must not read as test-scope"
3694 );
3695 }
3696
3697 /// Regression test for the bug in the *fix* for the above: a column-0-`}`
3698 /// shortcut (tried and reverted during review) truncates a test module the moment
3699 /// its body embeds a multi-line fixture string containing a `}` flush against the
3700 /// left margin — exactly `bynk-ide/src/sequence.rs`'s shape, whose test mod embeds
3701 /// `.bynk` source fixtures. The real brace-depth scanner must see through the
3702 /// string and find the module's *actual* closing brace, hundreds of lines later.
3703 /// Uses a raw string for the outer fixture so the embedded `"..."` doesn't need
3704 /// escaping, and locates the real end by content rather than a hand-counted index
3705 /// — a hand-counted line number is exactly the kind of easy-to-miscount detail
3706 /// this codebase's own convention (verify, don't assume) warns against.
3707 #[test]
3708 fn test_mod_ranges_is_not_fooled_by_a_column_zero_brace_inside_a_string() {
3709 let src = r#"#[cfg(test)]
3710mod tests {
3711 const FIXTURE: &str = "
3712commons app.demo {
3713}
3714";
3715
3716 fn real_end_of_module() {}
3717}
3718"#;
3719 let lines: Vec<&str> = src.lines().collect();
3720 let ranges = test_mod_ranges(&lines);
3721 assert_eq!(ranges.len(), 1, "expected exactly one range: {ranges:?}");
3722 let (_, end) = ranges[0];
3723 // `str::lines()` drops the trailing newline, so the module's real closing
3724 // brace — the fixture's last line — is at `lines.len() - 1`. The string's
3725 // embedded `}` (an earlier line) must not be mistaken for it.
3726 assert_eq!(
3727 end,
3728 lines.len() - 1,
3729 "closed too early — mistook the string's `}}` for the module's: {ranges:?}"
3730 );
3731 }
3732
3733 // --- fs_below_driver: import resolution through `use super::*;` (#1013) ---
3734
3735 /// Run [`production_std_fs_files`] over an in-memory crate layout and name the
3736 /// flagged files, so each case reads as "these files, and only these".
3737 fn flagged(files: &[(&str, &str)]) -> Vec<String> {
3738 let owned: Vec<(PathBuf, String)> = files
3739 .iter()
3740 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
3741 .collect();
3742 production_std_fs_files(&owned)
3743 .into_iter()
3744 .map(|i| files[i].0.to_string())
3745 .collect()
3746 }
3747
3748 /// The concrete #1013 instance, in miniature: `project.rs` has a module-level
3749 /// `use std::fs;` (ancestor-scoped, so visible to descendants), `discovery.rs`
3750 /// glob-imports it via `use super::*;` and calls bare `fs::read_to_string` —
3751 /// touching `std::fs` in production while never spelling it. The text scan alone
3752 /// reads only `project.rs`; the resolved probe must read both.
3753 #[test]
3754 fn bare_fs_reached_through_a_glob_imported_parent_is_flagged() {
3755 let files = [
3756 ("lib.rs", "mod project;\n"),
3757 ("project.rs", "use std::fs;\n\nmod discovery;\n"),
3758 (
3759 "project/discovery.rs",
3760 "use super::*;\n\nfn read_source(path: &std::path::Path) -> String {\n fs::read_to_string(path).unwrap()\n}\n",
3761 ),
3762 ];
3763 assert!(
3764 !has_production_std_fs(files[2].1),
3765 "the text scan alone must miss it"
3766 );
3767 assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
3768 }
3769
3770 /// Without `use super::*;` there is no path from the bare `fs::` to the parent's
3771 /// binding — the probe must not guess one into existence.
3772 #[test]
3773 fn bare_fs_without_a_glob_super_import_is_not_flagged() {
3774 let files = [
3775 ("lib.rs", "mod project;\n"),
3776 ("project.rs", "use std::fs;\n\nmod discovery;\n"),
3777 (
3778 "project/discovery.rs",
3779 "fn read_source(path: &std::path::Path) -> String {\n fs::read_to_string(path).unwrap()\n}\n",
3780 ),
3781 ];
3782 assert_eq!(flagged(&files), vec!["project.rs"]);
3783 }
3784
3785 /// Glob chains re-reach ancestors transitively — grandparent binds `fs`, both
3786 /// hops glob-import `super::*` — and the `mod.rs` layout maps to the same module
3787 /// tree as the `name.rs` one. The middle file sees `fs` but never uses it, so
3788 /// only the leaf joins the (text-flagged) root.
3789 #[test]
3790 fn glob_super_resolution_is_transitive_across_mod_rs_parents() {
3791 let files = [
3792 ("lib.rs", "use std::fs;\n\nmod a;\n"),
3793 ("a/mod.rs", "use super::*;\n\nmod b;\n"),
3794 (
3795 "a/b.rs",
3796 "use super::*;\n\nfn walk() {\n let _ = fs::read_dir(\".\");\n}\n",
3797 ),
3798 ];
3799 assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
3800 }
3801
3802 /// A break anywhere in the chain stops resolution: the middle module does not
3803 /// glob-import `super::*`, so the leaf's `use super::*;` reaches a module with no
3804 /// `fs` binding to offer.
3805 #[test]
3806 fn a_break_in_the_glob_chain_stops_resolution() {
3807 let files = [
3808 ("lib.rs", "use std::fs;\n\nmod a;\n"),
3809 ("a/mod.rs", "mod b;\n"),
3810 (
3811 "a/b.rs",
3812 "use super::*;\n\nfn walk() {\n let _ = fs::read_dir(\".\");\n}\n",
3813 ),
3814 ];
3815 assert_eq!(flagged(&files), vec!["lib.rs"]);
3816 }
3817
3818 /// Nearest binding wins, as in Rust: the child re-binds `fs` to something that is
3819 /// not `std::fs`, so its bare `fs::` calls are that something's, not std's.
3820 #[test]
3821 fn a_local_non_std_binding_shadows_the_ancestors_std_fs() {
3822 let files = [
3823 ("lib.rs", "mod project;\n"),
3824 ("project.rs", "use std::fs;\n\nmod overlay;\nmod d;\n"),
3825 ("project/overlay.rs", "pub fn read(_p: &str) {}\n"),
3826 (
3827 "project/d.rs",
3828 "use super::*;\nuse crate::project::overlay as fs;\n\nfn f() {\n let _ = fs::read(\"x\");\n}\n",
3829 ),
3830 ];
3831 assert_eq!(flagged(&files), vec!["project.rs"]);
3832 }
3833
3834 /// An aliased module binding resolves under its alias — the call site never
3835 /// contains the substring `fs::` at all.
3836 #[test]
3837 fn an_aliased_std_fs_binding_resolves_through_the_glob() {
3838 let files = [
3839 ("lib.rs", "mod p;\n"),
3840 ("p.rs", "use std::fs as stdfs;\n\nmod c;\n"),
3841 (
3842 "p/c.rs",
3843 "use super::*;\n\nfn f() {\n stdfs::write(\"a\", \"b\").unwrap();\n}\n",
3844 ),
3845 ];
3846 assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
3847 }
3848
3849 /// `use std::{fs, io};` binds `fs` without ever containing the substring
3850 /// `std::fs` — the same blind spot as #1013's, one file deep. Resolution applies
3851 /// in the file's own scope, no glob import required.
3852 #[test]
3853 fn a_group_imported_fs_binding_is_resolved_in_its_own_file() {
3854 let src = "use std::{fs, io};\n\nfn f() -> io::Result<()> {\n fs::metadata(\"x\").map(|_| ())\n}\n";
3855 assert!(
3856 !has_production_std_fs(src),
3857 "the text scan alone must miss it"
3858 );
3859 let files = [("thing.rs", src)];
3860 assert_eq!(flagged(&files), vec!["thing.rs"]);
3861 }
3862
3863 /// The item-import shape #1013 scope-checked (zero current instances), at the
3864 /// granularity this probe can reach: an ancestor's `use std::fs::File;` used as a
3865 /// bare path root `File::open` in a glob-importing child resolves and flags. (A
3866 /// bare *call* of an imported fn — `read_to_string(p)`, no `::` — presents no
3867 /// path root and remains out of a text-level scanner's reach, per the doc.)
3868 #[test]
3869 fn an_item_import_under_std_fs_resolves_as_a_path_root() {
3870 let files = [
3871 ("lib.rs", "mod p;\n"),
3872 ("p.rs", "use std::fs::File;\n\nmod c;\n"),
3873 (
3874 "p/c.rs",
3875 "use super::*;\n\nfn f() {\n let _ = File::open(\"x\");\n}\n",
3876 ),
3877 ];
3878 assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
3879 }
3880
3881 /// A test module's `use super::*;` and tempdir `fs::` calls are test-scope — the
3882 /// `bynk-ide` files' shape (`architecture.rs`, `sequence.rs`), which must stay
3883 /// unflagged exactly as they were under the text-only scan.
3884 #[test]
3885 fn glob_and_bare_fs_inside_a_test_mod_stay_test_scope() {
3886 let files = [
3887 ("lib.rs", "use std::fs;\n\nmod w;\n"),
3888 (
3889 "w.rs",
3890 "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",
3891 ),
3892 ];
3893 assert_eq!(flagged(&files), vec!["lib.rs"]);
3894 }
3895
3896 /// The module-tree mapping behind the resolution, checked directly: `name.rs` and
3897 /// `mod.rs` layouts, a preferred `a.rs` over `a/mod.rs`, and rootless roots.
3898 #[test]
3899 fn module_parent_maps_both_file_layouts() {
3900 let files: Vec<(PathBuf, String)> = ["lib.rs", "a.rs", "a/b.rs", "c/mod.rs", "c/d.rs"]
3901 .iter()
3902 .map(|p| (PathBuf::from(p), String::new()))
3903 .collect();
3904 let idx = |name: &str| {
3905 files
3906 .iter()
3907 .position(|(p, _)| p == Path::new(name))
3908 .unwrap()
3909 };
3910 assert_eq!(module_parent(Path::new("lib.rs"), &files), None);
3911 assert_eq!(
3912 module_parent(Path::new("a.rs"), &files),
3913 Some(idx("lib.rs"))
3914 );
3915 assert_eq!(
3916 module_parent(Path::new("a/b.rs"), &files),
3917 Some(idx("a.rs"))
3918 );
3919 assert_eq!(
3920 module_parent(Path::new("c/mod.rs"), &files),
3921 Some(idx("lib.rs"))
3922 );
3923 assert_eq!(
3924 module_parent(Path::new("c/d.rs"), &files),
3925 Some(idx("c/mod.rs"))
3926 );
3927 }
3928
3929 // --- fs_below_driver: #1016 review findings ------------------------------
3930
3931 /// Finding 1: a `super::`-qualified path needs no glob import — module privacy is
3932 /// ancestor-scoped, so `super::fs` names the parent's private `use std::fs;` from
3933 /// any child. One disambiguating edit away from `discovery.rs:39`'s bare call,
3934 /// and it must not drop the file out of the count.
3935 #[test]
3936 fn a_super_qualified_path_resolves_without_a_glob_import() {
3937 let files = [
3938 ("lib.rs", "mod project;\n"),
3939 ("project.rs", "use std::fs;\n\nmod discovery;\n"),
3940 (
3941 "project/discovery.rs",
3942 "fn read_source(path: &std::path::Path) -> String {\n super::fs::read_to_string(path).unwrap()\n}\n",
3943 ),
3944 ];
3945 assert_eq!(flagged(&files), vec!["project.rs", "project/discovery.rs"]);
3946 }
3947
3948 /// Finding 1, the `crate::`-rooted form: the walk descends the module tree from
3949 /// the crate root file by file, then resolves the leaf against that module's
3950 /// bindings — from anywhere in the crate, glob import or not.
3951 #[test]
3952 fn a_crate_qualified_path_resolves_through_the_module_tree() {
3953 let files = [
3954 ("lib.rs", "mod other;\nmod project;\n"),
3955 (
3956 "other.rs",
3957 "fn f() {\n let _ = crate::project::fs::read_dir(\".\");\n}\n",
3958 ),
3959 ("project.rs", "use std::fs;\n"),
3960 ];
3961 assert_eq!(flagged(&files), vec!["other.rs", "project.rs"]);
3962 }
3963
3964 /// Finding 1, stacked hops: `super::super::` climbs two parents (through a
3965 /// glob-free middle module — qualified paths don't need the glob chain).
3966 #[test]
3967 fn stacked_super_hops_climb_the_parent_chain() {
3968 let files = [
3969 ("lib.rs", "use std::fs;\n\nmod a;\n"),
3970 ("a/mod.rs", "mod b;\n"),
3971 (
3972 "a/b.rs",
3973 "fn f() {\n let _ = super::super::fs::read_dir(\".\");\n}\n",
3974 ),
3975 ];
3976 assert_eq!(flagged(&files), vec!["lib.rs", "a/b.rs"]);
3977 }
3978
3979 /// Finding 1, `self::` composed with the glob chain: `self::fs` resolves in the
3980 /// file's own namespace, which includes what its `use super::*;` pulled in.
3981 #[test]
3982 fn a_self_qualified_path_resolves_through_the_files_own_glob_chain() {
3983 let files = [
3984 ("lib.rs", "mod p;\n"),
3985 ("p.rs", "use std::fs;\n\nmod c;\n"),
3986 (
3987 "p/c.rs",
3988 "use super::*;\n\nfn f() {\n let _ = self::fs::read_dir(\".\");\n}\n",
3989 ),
3990 ];
3991 assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
3992 }
3993
3994 /// Finding 1's negatives: a qualified path to a name the parent binds to
3995 /// something other than `std::fs` stops at that binding, and a path through a
3996 /// module that doesn't exist resolves nowhere.
3997 #[test]
3998 fn a_qualified_path_to_a_non_std_binding_or_missing_module_is_not_flagged() {
3999 let files = [
4000 ("lib.rs", "mod overlay;\nmod p;\n"),
4001 ("overlay.rs", "pub fn read_dir(_p: &str) {}\n"),
4002 ("p.rs", "use crate::overlay as fs;\n\nmod d;\n"),
4003 (
4004 "p/d.rs",
4005 "fn f() {\n let _ = super::fs::read_dir(\".\");\n let _ = crate::missing::fs::read_dir(\".\");\n}\n",
4006 ),
4007 ];
4008 assert_eq!(flagged(&files), Vec::<String>::new());
4009 }
4010
4011 /// Finding 2: a locally-declared type-namespace item beats a glob-imported name
4012 /// in real Rust — a child with its own `mod fs;` calling `fs::…` is calling its
4013 /// own submodule, not the ancestor's `std::fs`.
4014 #[test]
4015 fn a_locally_declared_module_shadows_the_ancestors_std_fs() {
4016 let files = [
4017 ("lib.rs", "mod p;\n"),
4018 ("p.rs", "use std::fs;\n\nmod c;\n"),
4019 (
4020 "p/c.rs",
4021 "use super::*;\n\nmod fs;\n\nfn f() {\n let _ = fs::read_dir(\".\");\n}\n",
4022 ),
4023 ("p/c/fs.rs", "pub fn read_dir(_p: &str) {}\n"),
4024 ];
4025 assert_eq!(flagged(&files), vec!["p.rs"]);
4026 }
4027
4028 /// Finding 3: a trailing `//` comment on a `use` line must not sever the edge —
4029 /// neither the glob (`use super::*; // …`) nor the binding (`use std::fs; // …`).
4030 #[test]
4031 fn a_trailing_comment_on_a_use_line_does_not_sever_resolution() {
4032 let files = [
4033 ("lib.rs", "mod p;\n"),
4034 (
4035 "p.rs",
4036 "use std::fs; // read_source's disk fallback\n\nmod c;\n",
4037 ),
4038 (
4039 "p/c.rs",
4040 "use super::*; // parent's fs, PathBuf\n\nfn f() {\n let _ = fs::read_dir(\".\");\n}\n",
4041 ),
4042 ];
4043 assert_eq!(flagged(&files), vec!["p.rs", "p/c.rs"]);
4044 }
4045
4046 /// Finding 4: the nested-group + `::self` normalisation branches, pinned
4047 /// directly — `use std::{fs::{self, File}, io};` binds `fs` *and* `File` to
4048 /// `std::fs`, and `io` only to the shadow set. Getting `::self` wrong would
4049 /// silently under-count, which is exactly this probe's failure mode.
4050 #[test]
4051 fn a_nested_group_with_self_binds_the_module_and_its_items() {
4052 let facts = fs_import_facts("use std::{fs::{self, File}, io};\n");
4053 let bound: Vec<&str> = facts.std_fs_bindings.iter().map(String::as_str).collect();
4054 assert_eq!(bound, vec!["File", "fs"]);
4055 assert!(facts.use_bound_names.contains("io"));
4056 assert!(!facts.std_fs_bindings.contains("io"));
4057 }
4058
4059 /// Finding 4, the children half of [`FsImportFacts`]' contract: a parent whose
4060 /// *only* `use std::fs;` lives in its `#[cfg(test)] mod` hands no binding to a
4061 /// glob-importing child — `bynk-ide/src/symbols.rs`' shape, latent until it
4062 /// grows a submodule.
4063 #[test]
4064 fn a_parents_test_mod_use_std_fs_does_not_reach_its_children() {
4065 let files = [
4066 ("lib.rs", "mod p;\n"),
4067 (
4068 "p.rs",
4069 "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",
4070 ),
4071 (
4072 "p/c.rs",
4073 "use super::*;\n\nfn f() {\n let _ = fs::read_dir(\".\");\n}\n",
4074 ),
4075 ];
4076 assert_eq!(flagged(&files), Vec::<String>::new());
4077 }
4078
4079 // --- fs_below_driver: named-floor classification (#1104) -----------------
4080
4081 #[test]
4082 fn fn_name_on_line_strips_modifiers() {
4083 assert_eq!(fn_name_on_line("fn foo() {"), Some("foo".to_string()));
4084 assert_eq!(
4085 fn_name_on_line("pub(crate) fn read_adapter_binding("),
4086 Some("read_adapter_binding".to_string())
4087 );
4088 assert_eq!(
4089 fn_name_on_line("pub async unsafe fn go() {"),
4090 Some("go".to_string())
4091 );
4092 }
4093
4094 #[test]
4095 fn fn_name_on_line_ignores_non_fn_lines() {
4096 assert_eq!(fn_name_on_line(" let f = foo();"), None);
4097 assert_eq!(fn_name_on_line("/// calls fn bar somewhere"), None);
4098 }
4099
4100 /// A signature whose `{` arrives lines after the `fn` line — `read_adapter_binding`'s
4101 /// own real shape — must still resolve to the correct body range: `started` can't
4102 /// flip true on the parameter list, which has no braces of its own.
4103 #[test]
4104 fn production_fn_ranges_handles_a_wrapped_signature() {
4105 let src = "pub(crate) fn read_adapter_binding(\n path: &Path,\n) -> std::io::Result<String> {\n fs::read_to_string(path)\n}\n";
4106 let lines: Vec<&str> = src.lines().collect();
4107 let ranges = production_fn_ranges(&lines, &[]);
4108 assert_eq!(ranges.len(), 1);
4109 let (name, start, end) = &ranges[0];
4110 assert_eq!(name, "read_adapter_binding");
4111 assert_eq!(*start, 0);
4112 assert_eq!(*end, lines.len() - 1);
4113 assert_eq!(
4114 enclosing_fn(3, &ranges),
4115 Some("read_adapter_binding".to_string())
4116 );
4117 }
4118
4119 /// #1561: [`NAMED_FS_EXCEPTIONS`] itself is empty (P4.0 moved every file it used to
4120 /// name out of `bynk-emit` entirely — see the const's own doc comment), so the tests
4121 /// below that exercise a *match* use these synthetic tuples instead of real,
4122 /// currently-empty production data — same crate as the real #1104 shape, but a
4123 /// file/fn pair invented for this test and never a real production exception.
4124 const SYNTHETIC_EXCEPTIONS: &[(&str, &str, &str)] = &[
4125 (
4126 "bynk-emit",
4127 "project/synthetic_example.rs",
4128 "synthetic_named_fn_a",
4129 ),
4130 (
4131 "bynk-emit",
4132 "project/synthetic_example.rs",
4133 "synthetic_named_fn_b",
4134 ),
4135 ];
4136
4137 /// Build the `facts`/`parents` vectors [`file_is_named_fs_floor`] now takes as
4138 /// caller-supplied arguments, the same way [`fs_below_driver`] does, so each test
4139 /// below reads as "classify this file" rather than repeating the setup.
4140 fn classify(
4141 krate: &str,
4142 files: &[(PathBuf, String)],
4143 i: usize,
4144 exceptions: &[(&str, &str, &str)],
4145 ) -> bool {
4146 let facts: Vec<FsImportFacts> = files.iter().map(|(_, s)| fs_import_facts(s)).collect();
4147 let parents: Vec<Option<usize>> =
4148 files.iter().map(|(p, _)| module_parent(p, files)).collect();
4149 file_is_named_fs_floor(krate, files, &facts, &parents, i, exceptions)
4150 }
4151
4152 /// The concrete #1104 shape, in miniature (#1561: against [`SYNTHETIC_EXCEPTIONS`],
4153 /// `NAMED_FS_EXCEPTIONS` itself being empty): `project.rs`'s bare `use std::fs;` (no
4154 /// enclosing fn — never itself a violation) plus `synthetic_example.rs`'s two
4155 /// named-exception functions. The whole file must read as a named floor, not
4156 /// residual.
4157 #[test]
4158 fn file_is_named_fs_floor_true_for_a_file_of_only_named_exceptions() {
4159 let files = [
4160 (
4161 PathBuf::from("project.rs"),
4162 "use std::fs;\n\nmod synthetic_example;\n".to_string(),
4163 ),
4164 (
4165 PathBuf::from("project/synthetic_example.rs"),
4166 "use super::*;\n\npub(crate) fn synthetic_named_fn_a() {\n let _ = fs::read_dir(\".\");\n}\n\npub(crate) fn synthetic_named_fn_b(path: &Path) -> std::io::Result<String> {\n fs::read_to_string(path)\n}\n".to_string(),
4167 ),
4168 ];
4169 assert!(classify("bynk-emit", &files, 1, SYNTHETIC_EXCEPTIONS));
4170 }
4171
4172 /// A new, unlisted fn touching `std::fs` in the *same file* as two named exceptions
4173 /// must flip the whole file to residual — no partial credit, since "named floor"
4174 /// must mean every touch is accounted for, not most of them.
4175 #[test]
4176 fn file_is_named_fs_floor_false_when_an_unnamed_fn_also_touches_fs() {
4177 let files = [
4178 (
4179 PathBuf::from("project.rs"),
4180 "use std::fs;\n\nmod synthetic_example;\n".to_string(),
4181 ),
4182 (
4183 PathBuf::from("project/synthetic_example.rs"),
4184 "use super::*;\n\npub(crate) fn synthetic_named_fn_a() {\n let _ = fs::read_dir(\".\");\n}\n\nfn some_new_helper() {\n let _ = fs::write(\"x\", \"y\");\n}\n".to_string(),
4185 ),
4186 ];
4187 assert!(!classify("bynk-emit", &files, 1, SYNTHETIC_EXCEPTIONS));
4188 }
4189
4190 /// A file whose only production-scope touch is a bare `use std::fs;` import — no
4191 /// enclosing fn at all — is trivially a named floor: the import performs no I/O by
4192 /// itself, and the descendant it enables is checked (and named) separately. No
4193 /// named exceptions needed at all here — nothing to match against.
4194 #[test]
4195 fn file_is_named_fs_floor_true_for_an_import_only_file() {
4196 let files = [(
4197 PathBuf::from("project.rs"),
4198 "use std::fs;\n\nmod discovery;\n".to_string(),
4199 )];
4200 assert!(classify("bynk-emit", &files, 0, &[]));
4201 }
4202
4203 /// The same synthetic-example shape under the wrong crate label must not read as a
4204 /// floor — [`NAMED_FS_EXCEPTIONS`] is keyed on `(crate, file, fn)`, not `(file, fn)`
4205 /// alone, so a same-named file/fn pair in a different crate isn't accidentally
4206 /// covered.
4207 #[test]
4208 fn file_is_named_fs_floor_false_under_the_wrong_crate() {
4209 let files = [
4210 (
4211 PathBuf::from("project.rs"),
4212 "use std::fs;\n\nmod synthetic_example;\n".to_string(),
4213 ),
4214 (
4215 PathBuf::from("project/synthetic_example.rs"),
4216 "use super::*;\n\npub(crate) fn synthetic_named_fn_a() {\n let _ = fs::read_dir(\".\");\n}\n".to_string(),
4217 ),
4218 ];
4219 assert!(!classify("bynk-ide", &files, 1, SYNTHETIC_EXCEPTIONS));
4220 }
4221
4222 /// Review finding (#1106): a module-scope `std::fs` touch that isn't an import
4223 /// declaration — a `static` initialiser doing real I/O — has no enclosing fn either,
4224 /// but is a genuine R2.3 violation and must not be waved through as a floor just
4225 /// because it sits outside every known fn range.
4226 #[test]
4227 fn file_is_named_fs_floor_false_for_a_module_scope_static_that_reads() {
4228 let files = [(
4229 PathBuf::from("project.rs"),
4230 "use std::fs;\n\nstatic ROOT: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| fs::read_to_string(\"x\").unwrap());\n"
4231 .to_string(),
4232 )];
4233 assert!(!classify("bynk-emit", &files, 0, &[]));
4234 }
4235
4236 /// Same review finding, the [`fn_name_on_line`] half: an `extern "C" fn` (a modifier
4237 /// combination the parser doesn't strip) produces no [`production_fn_ranges`] entry
4238 /// at all, so its whole body would fall into the "no enclosing fn" branch. It must
4239 /// still read as residual, not floor, once it touches `std::fs`.
4240 #[test]
4241 fn file_is_named_fs_floor_false_for_an_unparsed_extern_fn_body() {
4242 let files = [(
4243 PathBuf::from("project.rs"),
4244 "use std::fs;\n\nextern \"C\" fn callback() {\n let _ = fs::read_dir(\".\");\n}\n"
4245 .to_string(),
4246 )];
4247 assert!(!classify("bynk-emit", &files, 0, &[]));
4248 }
4249
4250 /// #1587: [`NAMED_FS_EXCEPTIONS`] is empty today (#1561 cleared the three entries
4251 /// that outlived their files' P4.0 move out of `bynk-emit` for weeks, unnoticed,
4252 /// while `fs_below_driver`'s `0 named floor` reading stayed vacuously "healthy"),
4253 /// so this loop is vacuous now — but guards whatever named exception is decided
4254 /// next the same way [`ts_writes_excluded_files_still_exist`] and
4255 /// [`ast_importer_exceptions_still_exist_and_still_import_the_ast`] guard their own
4256 /// lists: a future entry going stale must fail loud here, not silently surface as
4257 /// a falsely-healthy `fs_below_driver` count. The tuple's third field is checked
4258 /// too (review of #1591), not just discarded — [`file_is_named_fs_floor`] matches
4259 /// on the full `(crate, file, fn)` key, so a refactor that keeps the file but
4260 /// renames or deletes the named function would otherwise leave a silently dead
4261 /// entry, the same recurrence shape #1561's three entries took.
4262 #[test]
4263 fn named_fs_exceptions_still_exist() {
4264 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
4265 for (krate, rel, fn_name) in NAMED_FS_EXCEPTIONS {
4266 let path = root.join(krate).join("src").join(rel);
4267 let contents = std::fs::read_to_string(&path).unwrap_or_else(|e| {
4268 panic!("NAMED_FS_EXCEPTIONS entry {krate:?}/{rel:?} does not exist: {e}")
4269 });
4270 assert!(
4271 contents.contains(fn_name),
4272 "NAMED_FS_EXCEPTIONS entry {krate:?}/{rel:?}/{fn_name:?} no longer names \
4273 a function in that file — it excludes nothing and should be removed"
4274 );
4275 }
4276 }
4277
4278 // --- emit_abi_shapes (#999 Decision E) ----------------------------------
4279
4280 /// A binding's ordinary capability-interface imports, and the emit-ABI tag-layout
4281 /// names, must not be flagged — the exact failure mode Decision E rebuilt the probe
4282 /// to avoid (the original single-allowlist definition read 29-33 here, not 1).
4283 ///
4284 /// Exercises the real production allowlists via [`is_enumerated_emit_abi_or_capability_surface`]
4285 /// — not a local re-declaration. A test with its own copy of `EMIT_ABI` would still
4286 /// pass if the real one lost an entry (e.g. deleting `Uuid` from the production
4287 /// list), proving nothing about the probe it claims to cover.
4288 #[test]
4289 fn emit_abi_shapes_does_not_flag_capability_or_tag_layout_imports() {
4290 let src = "import type { Clock, Fetch, Locale } from \"./bynk.js\";\n\
4291 import { FetchError, Uuid } from \"./bynk.js\";\n\
4292 import { Err, None, Ok, Some, type Option, type Result } from \"./runtime.js\";\n";
4293 let imports = ts_named_imports_from_runtime_modules(src);
4294 let leaks: Vec<&String> = imports
4295 .iter()
4296 .filter(|i| !is_enumerated_emit_abi_or_capability_surface(i))
4297 .collect();
4298 assert!(leaks.is_empty(), "unexpected leaks: {leaks:?}");
4299 }
4300
4301 /// The falsifier from #999 Decision E, checked directly: deleting an entry from the
4302 /// real production allowlist must be detectable by *some* test — this one flags
4303 /// `Uuid` as a leak the moment it's removed from [`EMIT_ABI`], which the test above
4304 /// (using the real const) would also start failing on.
4305 #[test]
4306 fn is_enumerated_checks_the_real_production_allowlist() {
4307 assert!(is_enumerated_emit_abi_or_capability_surface("Uuid"));
4308 assert!(is_enumerated_emit_abi_or_capability_surface("LocaleTag"));
4309 assert!(!is_enumerated_emit_abi_or_capability_surface(
4310 "negotiateLocale"
4311 ));
4312 }
4313
4314 /// The real, current-tree finding this probe exists to surface: `negotiateLocale`,
4315 /// a plain value helper from `./runtime.js` alongside the tag-layout constructors,
4316 /// is neither an enumerated emit-ABI shape nor a capability-interface import.
4317 #[test]
4318 fn emit_abi_shapes_flags_a_non_enumerated_runtime_helper() {
4319 let src = "import { Err, None, Ok, Some, negotiateLocale, type Option, type Result } from \"./runtime.js\";\n";
4320 let imports = ts_named_imports_from_runtime_modules(src);
4321 assert!(imports.contains(&"negotiateLocale".to_string()));
4322 }
4323
4324 /// `FetchError` is `import type` in one binding and a plain value import in
4325 /// another (`FetchError.Timeout`) — Decision E's rejected type-vs-value
4326 /// discriminator. Confirms the extractor treats both forms as the same identifier,
4327 /// so the allowlist check doesn't depend on which form a given file happens to use.
4328 #[test]
4329 fn ts_import_extraction_ignores_type_only_vs_value_distinction() {
4330 let type_only = "import type { FetchError } from \"./bynk.js\";\n";
4331 let value = "import { FetchError, Uuid } from \"./bynk.js\";\n";
4332 assert_eq!(
4333 ts_named_imports_from_runtime_modules(type_only),
4334 vec!["FetchError".to_string()]
4335 );
4336 assert!(ts_named_imports_from_runtime_modules(value).contains(&"FetchError".to_string()));
4337 }
4338
4339 // --- options_sources -----------------------------------------------------
4340
4341 #[test]
4342 fn struct_body_finds_a_field_by_name() {
4343 let src = "struct Foo {\n pub sources: Option<HashMap<PathBuf, String>>,\n pub other: bool,\n}\n";
4344 let body = struct_body(src, "Foo").expect("struct body found");
4345 assert!(body.contains("sources"));
4346 }
4347
4348 #[test]
4349 fn struct_body_does_not_match_an_unrelated_struct() {
4350 let src =
4351 "struct Bar {\n pub sources: bool,\n}\n\nstruct Foo {\n pub other: bool,\n}\n";
4352 let body = struct_body(src, "Foo").expect("struct body found");
4353 assert!(!body.contains("sources"));
4354 }
4355
4356 // --- render_table's "Rules closed" section (#1001) ------------------------
4357
4358 fn empty_report() -> Report {
4359 Report { probes: Vec::new() }
4360 }
4361
4362 /// The section is static text — no count, no existence check — precisely
4363 /// because nothing regenerates `design/greenfield-status.md` when `stamp`
4364 /// writes the ledger, so a computed count would silently go stale the
4365 /// moment the first `closes_rule` landed (the drift a first draft of this
4366 /// section introduced, caught in #1001's review). This test pins "static"
4367 /// as the actual behaviour, not just the intent in a comment.
4368 #[test]
4369 fn render_table_rules_closed_section_is_static_regardless_of_the_tree() {
4370 let out = render_table(&empty_report());
4371 assert!(out.contains("greenfield-status-rules.md"), "{out}");
4372 assert!(
4373 out.contains("may not exist yet"),
4374 "the wording must not claim to know whether the ledger exists: {out}"
4375 );
4376 }
4377
4378 // --- ts_writes / ts_any (P7.0, #1296; testability + widening, review of #1297) --
4379
4380 /// Run [`ts_writes_violations`] over an in-memory file list — mirrors
4381 /// [`flagged`]'s own role for `production_std_fs_files`.
4382 fn ts_writes_over(files: &[(&str, &str)]) -> usize {
4383 let owned: Vec<(PathBuf, String)> = files
4384 .iter()
4385 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
4386 .collect();
4387 ts_writes_violations(&owned)
4388 }
4389
4390 /// Run [`ts_any_violations`] over an in-memory file list.
4391 fn ts_any_over(files: &[(&str, &str)]) -> usize {
4392 let owned: Vec<(PathBuf, String)> = files
4393 .iter()
4394 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
4395 .collect();
4396 ts_any_violations(&owned)
4397 }
4398
4399 /// Run [`verbatim_origins_violations`] over an in-memory file list.
4400 fn verbatim_origins_over(files: &[(&str, &str)]) -> usize {
4401 let owned: Vec<(PathBuf, String)> = files
4402 .iter()
4403 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
4404 .collect();
4405 verbatim_origins_violations(&owned)
4406 }
4407
4408 /// Run [`verbatim_sites_violations`] over an in-memory file list.
4409 fn verbatim_sites_over(files: &[(&str, &str)]) -> usize {
4410 let owned: Vec<(PathBuf, String)> = files
4411 .iter()
4412 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
4413 .collect();
4414 verbatim_sites_violations(&owned)
4415 }
4416
4417 #[test]
4418 fn ts_writes_excluded_files_are_recognised() {
4419 assert!(is_ts_writes_excluded_file(Path::new("emitter/wrangler.rs")));
4420 assert!(is_ts_writes_excluded_file(Path::new("emitter/secrets.rs")));
4421 assert!(is_ts_writes_excluded_file(Path::new(
4422 "emitter/contracts.rs"
4423 )));
4424 assert!(is_ts_writes_excluded_file(Path::new("testkit.rs")));
4425 // Name proximity to a file that used to be excluded must not false-positive:
4426 // `emitter/lower.rs` (the emitter's own lowering pass) is genuinely
4427 // TS-producing and must stay counted — unlike `ir/lower.rs` (the checker→IR
4428 // pass), which isn't a name-proximity risk at all any more: it left
4429 // `bynk-emit/src` entirely at Arc D's P7.12 crate carve. `emitter/source_map.rs`
4430 // is the same shape (#1561): it left for `bynk-ts/src/source_map.rs` at P7.5
4431 // (#1308), so it's no longer excluded either — there's nothing left to exclude.
4432 assert!(!is_ts_writes_excluded_file(Path::new("emitter/lower.rs")));
4433 assert!(!is_ts_writes_excluded_file(Path::new("ir/lower.rs")));
4434 assert!(!is_ts_writes_excluded_file(Path::new(
4435 "emitter/source_map.rs"
4436 )));
4437 assert!(!is_ts_writes_excluded_file(Path::new("emitter.rs")));
4438 assert!(!is_ts_writes_excluded_file(Path::new("project.rs")));
4439 assert!(!is_ts_writes_excluded_file(Path::new(
4440 "project/tests_emit.rs"
4441 )));
4442 }
4443
4444 /// #1587: the exact failure #1561 fixed for [`NAMED_FS_EXCEPTIONS`] (an entry
4445 /// outliving its file by weeks while the gated probe kept reading a vacuous,
4446 /// falsely-healthy number) applies just as well to [`TS_WRITES_EXCLUDED_FILES`] —
4447 /// nothing previously caught a stale entry here either. Must fail loud, not surface
4448 /// as a silent `ts_writes`/`ts_any` regression in `greenfield_status_table_is_current`
4449 /// — mirrors [`ast_importer_exceptions_still_exist_and_still_import_the_ast`]'s own
4450 /// discipline for the sibling list: existence alone isn't enough (review of #1591)
4451 /// — a file that survives but stops containing any `write!`/`writeln!`/`format!`
4452 /// site would excludes nothing, and removing it would change no probe count, so
4453 /// [`ts_writes_violations`] must actually see a nonzero count over each entry's real
4454 /// content. Scored under a dummy, non-excluded path so the exclusion filter itself
4455 /// doesn't short-circuit the check.
4456 #[test]
4457 fn ts_writes_excluded_files_still_exist() {
4458 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4459 .join("..")
4460 .join("bynk-emit/src");
4461 for rel in TS_WRITES_EXCLUDED_FILES {
4462 let contents = std::fs::read_to_string(dir.join(rel)).unwrap_or_else(|e| {
4463 panic!("TS_WRITES_EXCLUDED_FILES entry {rel:?} does not exist: {e}")
4464 });
4465 let dummy = [(PathBuf::from("__not_excluded__.rs"), contents)];
4466 assert!(
4467 ts_writes_violations(&dummy) > 0,
4468 "TS_WRITES_EXCLUDED_FILES entry {rel:?} no longer contains a \
4469 write!/writeln!/format! call outside a test module — it excludes \
4470 nothing and should be removed"
4471 );
4472 }
4473 }
4474
4475 /// Regression for a real mistake this slice's own grounding found: an earlier
4476 /// survey during phase 7's own track-opening research treated
4477 /// `project/tests_emit.rs` as excludable "test-assertion" noise. It is real
4478 /// production code (`process_tests`/`process_integration_tests`) per
4479 /// `semantics-in-the-checker.md`'s own settling finding for a different probe on
4480 /// the same file. Exercises the real probe, not just the predicate (review of
4481 /// #1297 — a first cut of this test called `is_ts_writes_excluded_file` directly,
4482 /// which can't catch a bug in [`ts_writes_violations`]'s own use of it).
4483 #[test]
4484 fn ts_writes_does_not_exclude_tests_emit_rs_wholesale() {
4485 let count = ts_writes_over(&[(
4486 "project/tests_emit.rs",
4487 "fn process_tests() {\n let _ = format!(\"const x = 1;\");\n let _ = writeln!(out, \"const y = 2;\");\n}\n",
4488 )]);
4489 assert_eq!(
4490 count, 2,
4491 "tests_emit.rs's own production emission code must be counted, not excluded wholesale"
4492 );
4493 }
4494
4495 #[test]
4496 fn is_path_construction_line_catches_the_idiom_not_ordinary_format_calls() {
4497 assert!(is_path_construction_line(
4498 "let p = PathBuf::from(format!(\"workers/{dashes}/index.ts\"));"
4499 ));
4500 assert!(is_path_construction_line(
4501 "root.join(format!(\"tests/integration_{sanitized}.test.ts\"))"
4502 ));
4503 assert!(is_path_construction_line(
4504 "output_path.with_file_name(format!(\"{name}.{suffix}\"))"
4505 ));
4506 // An ordinary TS-producing `format!` call, no path construction, must not be
4507 // excluded by this idiom.
4508 assert!(!is_path_construction_line(
4509 "writeln!(out, \"{}\", format!(\"const {name} = 1;\"))"
4510 ));
4511 }
4512
4513 #[test]
4514 fn line_violates_ts_any_catches_the_cast_and_the_bare_annotation() {
4515 assert!(line_violates_ts_any("let x = (value as any).field;"));
4516 assert!(line_violates_ts_any("format!(\"{}: any\", name)"));
4517 assert!(line_violates_ts_any(
4518 "\"(seq: any[]) => ({ns} as any).drive(seq)\""
4519 ));
4520 assert!(!line_violates_ts_any("let x: unknown = value;"));
4521 }
4522
4523 /// Regression for review of #1297, finding 1: `any` in generic type-argument
4524 /// position (`Record<string, any[]>`, the live `emitter/lower.rs`
4525 /// `joinOn`/`leftJoin`/`groupBy` shape) contains neither `as any` nor `: any` and
4526 /// was silently uncounted by the round-one predicate.
4527 #[test]
4528 fn line_violates_ts_any_catches_generic_position_any() {
4529 assert!(line_violates_ts_any(
4530 "\"{{ const __h: Record<string, any[]> = {{}}; ...}}\""
4531 ));
4532 assert!(line_violates_ts_any("\"Array<any>\""));
4533 assert!(line_violates_ts_any("\"Promise<any>\""));
4534 // Must not regress the round-one patterns while widening.
4535 assert!(line_violates_ts_any("(value as any).field"));
4536 assert!(line_violates_ts_any("(e: any) => {}"));
4537 }
4538
4539 /// Regression for review of #1322, finding 2: once a site builds a real
4540 /// `bynk_ts::TsType` node instead of writing TypeScript text directly, the
4541 /// emitted `any` no longer appears as Rust-source `as any`/`: any` — the round-
4542 /// one/round-two patterns above all match *emitted-text* spellings, none of
4543 /// which appear in `TsType::named("any")`. `workers.rs`'s own three real sites
4544 /// (#1321) were silently uncounted until this pattern was added.
4545 #[test]
4546 fn line_violates_ts_any_catches_the_named_any_construction_spelling() {
4547 assert!(line_violates_ts_any(
4548 " let mut args = vec![as_expr(ident(\"payload\"), TsType::named(\"any\"))];"
4549 ));
4550 assert!(line_violates_ts_any(
4551 " Some(TsType::named(\"any\")),"
4552 ));
4553 // Must not regress the round-one/round-two patterns while widening.
4554 assert!(line_violates_ts_any("(value as any).field"));
4555 assert!(line_violates_ts_any("\"Array<any>\""));
4556 }
4557
4558 /// A comment mentioning either pattern in prose — the same self-reference-shaped
4559 /// hazard [`bynk_dotted_literals`]'s own regression tests guard against for a
4560 /// different probe — must not count.
4561 #[test]
4562 fn line_violates_ts_any_ignores_comments() {
4563 assert!(!line_violates_ts_any(
4564 "/// lowering machinery, same as any other subexpression."
4565 ));
4566 assert!(!line_violates_ts_any(
4567 "// TODO: stop emitting `: any` here once bynk-ts exists"
4568 ));
4569 }
4570
4571 /// `#[cfg(test)]`-gated write!-family calls (a file's own unit tests constructing a
4572 /// fixture string) must not count toward either probe — mirrors
4573 /// [`has_production_std_fs`]'s own test-range exclusion for a different probe.
4574 /// Exercises the real probes end to end, not a re-implementation of their loop
4575 /// (review of #1297, finding 2): deleting either probe's `in_test_range` guard, its
4576 /// `is_ts_writes_excluded_file` `continue`, or (for `ts_writes`) its
4577 /// `is_path_construction_line` `continue` now fails one of these tests.
4578 #[test]
4579 fn ts_writes_and_ts_any_exclude_cfg_test_ranges() {
4580 let src = "fn production() {\n let _ = format!(\"const x = 1;\");\n}\n\n\
4581 #[cfg(test)]\nmod tests {\n #[test]\n fn t() {\n \
4582 let _ = format!(\"(v as any)\");\n }\n}\n";
4583 assert_eq!(
4584 ts_writes_over(&[("emitter.rs", src)]),
4585 1,
4586 "only the production format! call counts"
4587 );
4588 assert_eq!(
4589 ts_any_over(&[("emitter.rs", src)]),
4590 0,
4591 "the test-only `as any` site must be excluded"
4592 );
4593 }
4594
4595 /// Exercises the real probes' file-exclusion `continue`, not just the predicate:
4596 /// a whole file on [`TS_WRITES_EXCLUDED_FILES`] must contribute 0 to either count
4597 /// even when its content would otherwise match both.
4598 #[test]
4599 fn ts_writes_and_ts_any_exclude_named_non_ts_files_end_to_end() {
4600 let files = [(
4601 "emitter/wrangler.rs",
4602 "fn write_toml(out: &mut String) {\n let _ = writeln!(out, \"name = {v}\");\n let __x: any = 1;\n}\n",
4603 )];
4604 assert_eq!(ts_writes_over(&files), 0);
4605 assert_eq!(ts_any_over(&files), 0);
4606 }
4607
4608 /// Exercises the real probes' [`is_path_construction_line`] `continue` end to end,
4609 /// not just the predicate in isolation.
4610 #[test]
4611 fn ts_writes_excludes_path_construction_end_to_end() {
4612 let files = [(
4613 "project.rs",
4614 "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",
4615 )];
4616 assert_eq!(
4617 ts_writes_over(&files),
4618 1,
4619 "the path-construction line must not count; the genuine emission line must"
4620 );
4621 }
4622
4623 #[test]
4624 fn verbatim_origins_counts_distinct_variants_not_construction_sites() {
4625 let files = [(
4626 "emitter/contracts.rs",
4627 "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",
4628 )];
4629 // Three construction sites, but only two distinct origins.
4630 assert_eq!(verbatim_origins_over(&files), 2);
4631 assert_eq!(verbatim_sites_over(&files), 3);
4632 }
4633
4634 #[test]
4635 fn verbatim_origins_and_sites_ignore_comments() {
4636 let files = [(
4637 "emitter/contracts.rs",
4638 "// TsStmt::verbatim(VerbatimOrigin::Contracts, \"x\", None)\n/// Mentions VerbatimOrigin::Secrets in prose.\n",
4639 )];
4640 assert_eq!(verbatim_origins_over(&files), 0);
4641 assert_eq!(verbatim_sites_over(&files), 0);
4642 }
4643
4644 #[test]
4645 fn verbatim_origins_and_sites_read_zero_over_an_empty_tree() {
4646 let files: [(&str, &str); 0] = [];
4647 assert_eq!(verbatim_origins_over(&files), 0);
4648 assert_eq!(verbatim_sites_over(&files), 0);
4649 }
4650
4651 /// Review of #1308, finding 6: without stripping `#[cfg(test)]` ranges,
4652 /// a single `bynk-emit` unit test fixture constructing a `TsStmt::
4653 /// verbatim(...)` for its own coverage would pin `verbatim_sites` above
4654 /// its documented 0 floor permanently, for a reason unrelated to
4655 /// residual production emission.
4656 #[test]
4657 fn verbatim_origins_and_sites_exclude_cfg_test_ranges() {
4658 let src = "fn production() {\n TsStmt::verbatim(VerbatimOrigin::Contracts, \"x\", None);\n}\n\n\
4659 #[cfg(test)]\nmod tests {\n #[test]\n fn t() {\n \
4660 TsStmt::verbatim(VerbatimOrigin::Secrets, \"y\", None);\n }\n}\n";
4661 assert_eq!(
4662 verbatim_origins_over(&[("emitter/contracts.rs", src)]),
4663 1,
4664 "only the production-code origin counts"
4665 );
4666 assert_eq!(
4667 verbatim_sites_over(&[("emitter/contracts.rs", src)]),
4668 1,
4669 "the test-only construction site must be excluded"
4670 );
4671 }
4672
4673 /// #1539: `verbatim_sites` widened from a `TsStmt::verbatim(`-only scan
4674 /// to also count `TsExpr::VerbatimExpr(` construction sites — the same
4675 /// escape hatch, now closed at the expression level too. Pins that both
4676 /// needles are counted (and both still respect the comment/`#[cfg(test)]`
4677 /// exclusions the sibling tests above already establish for the
4678 /// `TsStmt` half).
4679 #[test]
4680 fn verbatim_sites_counts_the_expr_level_needle_too() {
4681 let src = "fn f() {\n let x = TsExpr::VerbatimExpr(\"a\".to_string(), VerbatimOrigin::Emit);\n let y = TsStmt::verbatim(VerbatimOrigin::Emit, \"b\", None);\n}\n";
4682 assert_eq!(
4683 verbatim_sites_over(&[("emitter/emit.rs", src)]),
4684 2,
4685 "one TsExpr::VerbatimExpr( site plus one TsStmt::verbatim( site"
4686 );
4687 assert_eq!(
4688 verbatim_origins_over(&[("emitter/emit.rs", src)]),
4689 1,
4690 "both reference the same VerbatimOrigin::Emit variant"
4691 );
4692 }
4693
4694 // --- incremental_query_types (P8.0, #1510; re-settled by #1537) ----------
4695
4696 fn owned(files: &[(&str, &str)]) -> Vec<(PathBuf, String)> {
4697 files
4698 .iter()
4699 .map(|(p, s)| (PathBuf::from(p), (*s).to_string()))
4700 .collect()
4701 }
4702
4703 #[test]
4704 fn unit_signature_present_recognises_the_real_struct_and_ignores_a_comment() {
4705 assert!(unit_signature_present(&owned(&[(
4706 "unit_signature.rs",
4707 "pub struct UnitSignature {\n types: HashMap<String, Arc<TypeDecl>>,\n}\n",
4708 )])));
4709 assert!(!unit_signature_present(&owned(&[(
4710 "lib.rs",
4711 "// TODO: build a struct UnitSignature here eventually\n",
4712 )])));
4713 assert!(!unit_signature_present(&[]));
4714 }
4715
4716 /// The committed reading after #1537: neither deleted level is back.
4717 #[test]
4718 fn deleted_levels_present_is_empty_when_neither_level_exists() {
4719 assert!(deleted_levels_present(&[]).is_empty());
4720 }
4721
4722 /// Re-adding `ProjectGraph` anywhere flips the reading — the whole workspace is
4723 /// scanned (review of #1582) because P8.3 (ADR 0415) already landed it in a
4724 /// different crate from the one first assumed, and R3.13's table names a third
4725 /// (`bynk-resolve`) that does not exist yet.
4726 #[test]
4727 fn deleted_levels_present_sees_project_graph_wherever_it_lands() {
4728 let graph = "pub struct ProjectGraph {\n units: HashMap<UnitId, Unit>,\n}\n";
4729 assert_eq!(
4730 deleted_levels_present(&owned(&[("project_graph.rs", graph)])),
4731 vec!["ProjectGraph"]
4732 );
4733 assert_eq!(
4734 deleted_levels_present(&owned(&[("resolve/graph.rs", graph)])),
4735 vec!["ProjectGraph"]
4736 );
4737 assert!(
4738 deleted_levels_present(&owned(&[(
4739 "lib.rs",
4740 "// a struct ProjectGraph used to live here\n"
4741 )]))
4742 .is_empty()
4743 );
4744 }
4745
4746 /// **The empirically-confirmed false positive #1510's own first run caught** (see
4747 /// [`defid_query_fn_present`]): `bynk-check/src/checker.rs` has an ordinary,
4748 /// pre-existing `fn type_of(expr: &Expr, ..)` with no `DefId` anywhere in its
4749 /// signature. After #1537 the direction of the mistake reverses — it would now
4750 /// read as the deleted level having been *re-added* — so it is pinned against the
4751 /// real function's own signature text here too.
4752 #[test]
4753 fn deleted_levels_present_does_not_count_checkers_pre_existing_type_of() {
4754 let found = deleted_levels_present(&owned(&[(
4755 "checker.rs",
4756 "pub(crate) fn type_of(expr: &Expr, expected: Option<TyId>, ctx: &mut Ctx) -> Option<TyId> {\n",
4757 )]));
4758 assert!(
4759 found.is_empty(),
4760 "checker.rs's own type_of has no DefId parameter and must not count: {found:?}"
4761 );
4762 }
4763
4764 #[test]
4765 fn deleted_levels_present_sees_a_real_defid_keyed_body_and_type_of() {
4766 let queries = "pub fn body(id: DefId) -> Body {\n todo!()\n}\n\npub fn type_of(id: DefId) -> TypeOf {\n todo!()\n}\n";
4767 assert_eq!(
4768 deleted_levels_present(&owned(&[("queries.rs", queries)])),
4769 vec!["Body", "TypeOf"]
4770 );
4771 }
4772
4773 #[test]
4774 fn shared_cache_migrated_is_false_while_project_unit_cache_still_exists() {
4775 let ide_src: Vec<(PathBuf, String)> = vec![(
4776 PathBuf::from("completion.rs"),
4777 "static PROJECT_UNIT_CACHE: LazyLock<Mutex<HashMap<PathBuf, CachedUnit>>> = ..;"
4778 .to_string(),
4779 )];
4780 let project_src: Vec<(PathBuf, String)> = vec![(
4781 PathBuf::from("cache.rs"),
4782 "pub struct SharedUnitCache { .. }".to_string(),
4783 )];
4784 assert!(!shared_cache_migrated(&ide_src, &project_src));
4785 }
4786
4787 #[test]
4788 fn shared_cache_migrated_is_true_once_project_unit_cache_is_gone_and_a_shared_cache_lands() {
4789 let ide_src: Vec<(PathBuf, String)> = vec![(
4790 PathBuf::from("completion.rs"),
4791 "fn cached_project_unit(path: &Path, content: &str) -> Option<Arc<SourceUnit>> { .. }"
4792 .to_string(),
4793 )];
4794 let project_src: Vec<(PathBuf, String)> = vec![(
4795 PathBuf::from("cache.rs"),
4796 "pub struct SharedUnitCache { units: HashMap<FileId, Arc<SourceUnit>> }".to_string(),
4797 )];
4798 assert!(shared_cache_migrated(&ide_src, &project_src));
4799 }
4800
4801 /// **The real hole finding 1 caught**: absence of `PROJECT_UNIT_CACHE` from
4802 /// `bynk-ide` alone used to read "migrated" even when nothing shared replaced it —
4803 /// a rename or deletion with no cache anywhere in `bynk-project` satisfied the old
4804 /// clause. Now requires a cache-shaped item to actually land in
4805 /// `bynk-project/src` too.
4806 #[test]
4807 fn shared_cache_migrated_is_false_when_project_unit_cache_is_gone_but_nothing_shared_replaces_it()
4808 {
4809 let ide_src: Vec<(PathBuf, String)> = vec![(
4810 PathBuf::from("completion.rs"),
4811 "fn cached_project_unit(path: &Path, content: &str) -> Option<Arc<SourceUnit>> { .. }"
4812 .to_string(),
4813 )];
4814 assert!(!shared_cache_migrated(&ide_src, &[]));
4815 }
4816
4817 /// The needle is anchored on `static PROJECT_UNIT_CACHE`, not a bare substring —
4818 /// `PROJECT_UNIT_CACHE_CAP` (a real, unrelated `const` in
4819 /// `bynk-ide/src/completion.rs`) must not hold this false on its own.
4820 #[test]
4821 fn shared_cache_migrated_is_not_confused_by_project_unit_cache_cap() {
4822 let ide_src: Vec<(PathBuf, String)> = vec![(
4823 PathBuf::from("completion.rs"),
4824 "const PROJECT_UNIT_CACHE_CAP: usize = 4096;".to_string(),
4825 )];
4826 let project_src: Vec<(PathBuf, String)> = vec![(
4827 PathBuf::from("cache.rs"),
4828 "pub struct SharedUnitCache { .. }".to_string(),
4829 )];
4830 assert!(shared_cache_migrated(&ide_src, &project_src));
4831 }
4832
4833 #[test]
4834 fn stability_test_present_recognises_a_matching_test_name() {
4835 let check_tests: Vec<(PathBuf, String)> = vec![(
4836 PathBuf::from("unit_signature.rs"),
4837 "#[test]\nfn unit_signature_is_stable_under_a_body_edit() { .. }\n".to_string(),
4838 )];
4839 assert!(stability_test_present(&check_tests));
4840 }
4841
4842 #[test]
4843 fn stability_test_present_recognises_test_attribute_separated_by_other_attributes() {
4844 let check_tests: Vec<(PathBuf, String)> = vec![(
4845 PathBuf::from("unit_signature.rs"),
4846 "#[test]\n#[should_panic]\nfn unit_signature_panics_when_stability_is_violated() { .. }\n"
4847 .to_string(),
4848 )];
4849 assert!(stability_test_present(&check_tests));
4850 }
4851
4852 #[test]
4853 fn stability_test_present_is_false_for_an_unrelated_test() {
4854 let check_tests: Vec<(PathBuf, String)> = vec![(
4855 PathBuf::from("differential_analysis.rs"),
4856 "#[test]\nfn new_entry_point_matches_analyse_project_with() { .. }\n".to_string(),
4857 )];
4858 assert!(!stability_test_present(&check_tests));
4859 }
4860
4861 #[test]
4862 fn stability_test_present_ignores_a_comment_mentioning_it() {
4863 let check_tests: Vec<(PathBuf, String)> = vec![(
4864 PathBuf::from("lib.rs"),
4865 "// TODO: add a unit_signature stability test (P8.2)\n".to_string(),
4866 )];
4867 assert!(!stability_test_present(&check_tests));
4868 }
4869
4870 /// **The real hole finding 3 caught**: the old match required only
4871 /// `fn `+`unit_signature`+`stab` on one line, with no check for an actual
4872 /// `#[test]` attribute — a plain, non-test helper used to satisfy the clause with
4873 /// no passing test in existence.
4874 #[test]
4875 fn stability_test_present_is_false_for_a_non_test_helper_with_a_matching_name() {
4876 let check_tests: Vec<(PathBuf, String)> = vec![(
4877 PathBuf::from("unit_signature.rs"),
4878 "fn unit_signature_stability_fixture(edit: &Edit) -> UnitSignature { .. }\n"
4879 .to_string(),
4880 )];
4881 assert!(!stability_test_present(&check_tests));
4882 }
4883}