candor_classify/lib.rs
1//! candor-classify — the curated effect classifier (crate+path -> effect), extracted to a STABLE
2//! crate so both the nightly `rustc_private` lint AND a stable backend share ONE source of truth
3//! (no drift). Pure string logic; no rustc internals. The effect vocabulary lives in candor-report.
4
5use candor_report::EFFECTS;
6
7/// The canonical CANDOR_POLICY DSL parser (SPEC §6.2), shared by the nightly gate and candor-query.
8pub mod policy;
9#[cfg(test)]
10mod policy_props;
11
12/// The SURPRISE heuristic (the cold-repo hook) — SHARED so candor-scan's scan-time note and
13/// candor-query's `tour` verb can't drift. Generic over the effect element type.
14pub mod surface;
15
16/// The transitive least fixed point over a call graph — SHARED so the scanner's gate-side reason-class
17/// accumulator and candor-query's `unverified --class` filter resolve over the SAME reach.
18pub mod propagate;
19
20/// ⟨0.24⟩ The §6.2 GATE over an already-accumulated signature — SHARED so `candor-scan --policy` and
21/// `candor-query gate --report` (SPEC §3.1) are the same gate reached by two routes, not two gates.
22pub mod gate;
23
24/// ⟨0.41⟩ `accept` lines for known policy violations (SPEC §6.2) — matched over a whole run's rows.
25pub mod accept;
26
27/// Project-supplied rules, consulted only when the built-in `classify` returns None.
28pub fn classify_extra(
29 crate_name: &str,
30 path: &str,
31 extra: &[(&'static str, bool, String)],
32) -> Option<&'static str> {
33 for (eff, is_crate, prefix) in extra {
34 let hit = if *is_crate { crate_name.starts_with(prefix.as_str()) } else { path.starts_with(prefix.as_str()) };
35 if hit {
36 return Some(eff);
37 }
38 }
39 None
40}
41
42/// The exact third-party crates `classify` has effect rules for, and the crate-name
43/// PREFIXES it recognizes. This is the single source of truth for "what candor knows":
44/// it is emitted beside the JSON report (`<prefix>.calibrated.json`) so the Claude Code
45/// receipt's coverage check reads candor's real coverage instead of a hand-copied list.
46/// Keep in lockstep with `classify` below — the `db_crates_are_calibrated` and
47/// `calibrated_crates_are_live` tests (in this crate's `tests` module) enforce both directions.
48pub const CALIBRATED_CRATES: [&str; 82] = [
49 // network (aws_config resolves credentials over the network on `.load()`;
50 // git2 remote ops — fetch/push/connect — contact the network; async_net is smol's net layer;
51 // pnet is raw L2/L3 packet capture)
52 "reqwest", "isahc", "ureq", "curl", "aws_config", "git2", "tokio_tcp", "tokio_udp", "async_net",
53 "async_nats", "lapin", "lettre", "tungstenite", "elasticsearch", "tonic", "rdkafka", "pnet",
54 // directory traversal (ignore = gitignore-aware walker, powers ripgrep/fd; its walk executors are Fs)
55 // + filesystem watching (notify = inotify/FSEvents/kqueue wrapper; powers watchexec/cargo-watch)
56 "ignore", "notify",
57 // database (see DB_CRATES in classify)
58 "sqlx", "rusqlite", "postgres", "tokio_postgres", "diesel", "redis", "mongodb",
59 "mysql", "mysql_async", "sea_orm", "deadpool_postgres",
60 // filesystem (async_fs = smol; fs_err = std::fs wrapper; tempfile; glob) / entropy /
61 // subprocess (async_process = smol; duct) / env (dotenvy/dotenv) / clock (time) / log / clipboard
62 "memmap2", "fs_err", "async_fs", "tempfile", "glob",
63 "rand", "getrandom", "fastrand",
64 // entropy: the password-hashing tier (salt mints + bcrypt's internal salt) + the OsRng source
65 "argon2", "bcrypt", "scrypt", "pbkdf2", "password_hash", "rand_core",
66 "portable_pty", "async_process", "duct",
67 "dotenvy", "dotenv",
68 "chrono", "time", "tracing", "log", "arboard",
69 // compiler diagnostic emission (a dylint lint's output) — see the Log rules in classify
70 "rustc_lint", "rustc_errors",
71 // raw syscalls via FFI — the syscall-name table that lights up the FFI-thin tier (nix is routed
72 // through the same table by leaf name, so a consumer of nix is covered without nix's own source)
73 "libc", "nix", "rustix",
74 // coverage-differential additions (verb-keyed; see the per-crate rules near the end of classify):
75 // sync TLS core + native-tls variants (Net); env/dir resolution + argv + LS_COLORS (Env);
76 // sqlx-core execution terminals (Net/Db); directory walk + timestamp mutation + same-file (Fs);
77 // process-spawn helpers (Exec); signal handler + interactive-tty prompts (Ipc); env_logger (Log);
78 // jiff/backoff clock reads (Clock).
79 "rustls", "native_tls_crate", "tokio_native_tls",
80 "etcetera", "wild", "lscolors",
81 "sqlx_core", "walkdir", "filetime", "clircle",
82 "execute", "ctrlc", "clap", "jiff", "env_logger",
83 "dialoguer", "console", "terminal_colorsaurus", "backoff", "grep_cli",
84 // TUI: the terminal is a user dialogue channel (Ipc), exactly as dialoguer/console already rule.
85 // crossterm does the tty I/O; ratatui renders to a Buffer and drives a backend that does.
86 "crossterm", "ratatui",
87 // tracing_subscriber: the fmt INIT terminals write program output (Log); the EnvFilter constructors
88 // read RUST_LOG (Env). Everything else — layers, formatters, filter types — is a builder.
89 "tracing_subscriber",
90];
91
92pub const CALIBRATED_PREFIXES: [&str; 3] = ["aws_sdk_", "aws_smithy", "cap_"];
93
94/// Crates `classify` matches by PATH prefix rather than crate-name equality (their effectful modules
95/// are recognised, e.g. `tokio::net::`/`async_std::fs::`/`mio::net::`), so they're absent from
96/// `CALIBRATED_CRATES` (which the liveness test probes by crate name). The coverage check must still
97/// treat them as *covered* — otherwise it would mislabel the most common async crates as blind spots.
98pub const PATH_CALIBRATED_CRATES: [&str; 3] = ["tokio", "async_std", "mio"];
99
100/// The FFI-thin syscall crates whose `CALIBRATED_CRATES` membership is INCOMPLETE BY DESIGN.
101///
102/// For every OTHER calibrated crate, "classify returned None" is a REVIEWED verdict — the table covers
103/// the crate's effectful surface, so a miss means the call really is pure, and candor-scan's coverage
104/// ledger exempts all of `CALIBRATED_CRATES` from blind-spot disclosure on exactly that theory (a
105/// calibrated crate's silence is informative). That does not hold for `libc`/`nix`/`rustix`: the table
106/// above DELIBERATELY skips their generic fd verbs (`read`/`write`/`close`/`lseek`/`dup`/`fcntl`/...)
107/// because a fixed label would mis-categorise an ambiguous fd (file? socket? pipe?) as often as it
108/// helps — an honest no-classify, not a purity finding. Real programs that touch these crates go
109/// through those exact verbs constantly (`libc::read(fd, ..)` IS the effect, not a builder step), so the
110/// blanket exemption converted "no rule here" into a confident, silent purity claim: `fn drain(fd: i32)
111/// { libc::read(fd, buf, 64); }` reported zero effects AND zero disclosure (R59, SOUNDNESS.md) — worse
112/// than an uncalibrated dependency, which at least discloses `invisible`.
113///
114/// Consumed ONLY by candor-scan's coverage-ledger filter (the same one-consumer site
115/// `coverage_has_exactly_one_anchor_and_exactly_one_consumer` enforces), to carve these three OUT of the
116/// `CALIBRATED_CRATES` exemption there — an unclassified call into them still joins the ledger and
117/// disclosed `invisible`, exactly like a call into an uncalibrated dependency, while their CLASSIFIED
118/// calls (`open`/`close`/`socket`/...) are untouched (they never reach the ledger at all — see
119/// `blind_direct`'s `classified.is_none()` gate). Deliberately NOT removed from `CALIBRATED_CRATES`
120/// itself: that list means "classify has SOME live rules here" (true — the FS/NET/EXEC/IPC/ENV/CLOCK/
121/// RAND tables all fire for these three), and stays the honest `.calibrated.json` sidecar contract.
122pub const CALIBRATED_BUT_PARTIAL_CRATES: [&str; 3] = ["libc", "nix", "rustix"];
123
124/// SOUNDNESS R311 — the version each calibrated crate's rules were actually WRITTEN AGAINST.
125///
126/// `CALIBRATED_CRATES` converts "no rule matched" into "reviewed, and pure", with no disclosure. That
127/// claim is evidence about SOURCE SOMEONE READ, and it was being granted on the crate NAME alone — so
128/// an upstream release extended it, silently, to code that did not exist when the review happened.
129/// Measured 2026-09-07: the sea_orm rule block says in terms that it was "verified against sea-orm
130/// 1.1.20 source"; the calibrated fixture resolved 2.0.2, whose `DatabaseExecutor`,
131/// `RestrictedConnection`, `RestrictedTransaction`, `RbacContext` and `RusqliteSharedConnection` carry
132/// 43 public entry points that no rule can name and every one of which read PURE.
133///
134/// A crate resolving ABOVE its ceiling is downgraded to `CALIBRATED_BUT_PARTIAL_CRATES` behaviour:
135/// matched calls keep their rules (they are still right), unmatched calls rejoin the ledger and
136/// disclose `invisible` instead of reading as reviewed-pure. Fail-closed AT THE POINT THE CLAIM IS
137/// MADE — the coverage gate can only ever notify us afterwards.
138///
139/// EXACT, not major-only, and the direction is deliberate: an exact ceiling fails NOISY (a minor bump
140/// discloses until someone re-reviews and moves the line) while major-only fails SILENT — and the
141/// minor direction is not hypothetical, since redis 1.6.0 -> 1.7.0 and mongodb 3.8.1 -> 3.9.0 both
142/// produced real uncovered entry points in the same measurement. Per the family's denylist rule, start
143/// strict and relax against measured disclosure volume, never the reverse.
144///
145/// A crate ABSENT from this table keeps its unconditional exemption: the ceiling is a denylist of
146/// versions known to be beyond the review, not an allowlist of versions blessed by it. Adding a crate
147/// here is what makes its claim honest, so the table is expected to grow toward `CALIBRATED_CRATES`.
148// SOUNDNESS R328 — this table is MERGED, never rewritten. `bbd7615` replaced it wholesale with a
149// freshly-computed dict and silently dropped NINE entries (dialoguer, dotenv, dotenvy, grep_cli, isahc,
150// lettre, portable_pty, tempfile, postgres) while its own commit message claimed "four crates are
151// deliberately absent, each with a stated reason in the code". Eight had no stated reason and every one
152// of them cites a version in its own rules — they are exactly the crates the ceiling exists to protect,
153// and the protection would have been gone the moment any of them published. Effect classes at stake:
154// Net (isahc), SMTP (lettre), Exec (portable_pty), Fs (tempfile), Env (dotenv/dotenvy), Ipc (dialoguer).
155//
156// DELIBERATELY ABSENT, and this is the complete list:
157// postgres — R313. Its recorded 0.7.10 was TOKIO-postgres's version, lifted by a regex from a comment
158// about a different crate; postgres ships 0.19.x. No verified baseline exists and inventing
159// one is the claim-without-review this table exists to stop.
160// async_fs, getrandom — their rules match on CRATE NAME alone, so no path can go stale.
161// rand_core — R320/R333 unresolved.
162pub const CALIBRATED_CEILINGS: [(&str, &str); 70] = [
163 ("arboard", "3.6.1"),
164 ("argon2", "0.6.0"),
165 ("async_nats", "0.35.1"),
166 ("async_process", "2.5.0"),
167 ("aws_config", "1.12.0"),
168 ("backoff", "0.4.0"),
169 ("bcrypt", "0.19.3"),
170 ("chrono", "0.4.45"),
171 ("clap", "4.6.6"),
172 ("clircle", "0.6.1"),
173 ("console", "0.15.11"),
174 ("crossterm", "0.28.1"),
175 ("ctrlc", "3.5.2"),
176 ("curl", "0.4.50"),
177 ("deadpool_postgres", "0.14.2"),
178 ("dialoguer", "0.12.0"),
179 ("diesel", "2.3.13"),
180 ("dotenv", "0.15.0"),
181 ("dotenvy", "0.15.7"),
182 ("duct", "1.1.2"),
183 ("elasticsearch", "8.19.0-alpha.1"),
184 ("env_logger", "0.11.11"),
185 ("etcetera", "0.11.0"),
186 ("execute", "0.3.0"),
187 ("fastrand", "2.5.0"),
188 ("filetime", "0.2.29"),
189 ("fs_err", "3.3.1"),
190 ("git2", "0.20.4"),
191 ("glob", "0.3.4"),
192 ("grep_cli", "0.1.12"),
193 ("ignore", "0.4.33"),
194 ("isahc", "2.0.1"),
195 ("jiff", "0.2.35"),
196 ("lapin", "4.10.0"),
197 ("lettre", "0.11.23"),
198 ("log", "0.4.34"),
199 ("lscolors", "0.21.0"),
200 ("memmap2", "0.9.11"),
201 ("mongodb", "3.8.1"),
202 ("mysql", "28.0.0"),
203 ("mysql_async", "0.37.0"),
204 ("native_tls_crate", "0.2.18"),
205 ("notify", "8.2.0"),
206 ("password_hash", "0.6.1"),
207 ("pbkdf2", "0.13.0"),
208 ("pnet", "0.35.0"),
209 ("portable_pty", "0.9.0"),
210 ("rand", "0.10.2"),
211 ("ratatui", "0.29.0"),
212 ("rdkafka", "0.39.0"),
213 ("redis", "1.6.0"),
214 ("reqwest", "0.13.4"),
215 ("rusqlite", "0.39"),
216 ("rustls", "0.23.43"),
217 ("scrypt", "0.12.0"),
218 ("sea_orm", "1.1.20"),
219 ("sqlx", "0.9.0"),
220 ("sqlx_core", "0.9.0"),
221 ("tempfile", "3.27.0"),
222 ("terminal_colorsaurus", "1.0.3"),
223 ("time", "0.3.55"),
224 ("tokio_native_tls", "0.3.1"),
225 ("tokio_postgres", "0.7.18"),
226 ("tonic", "0.12.3"),
227 ("tracing", "0.1.44"),
228 ("tracing_subscriber", "0.3.23"),
229 ("tungstenite", "0.30.0"),
230 ("ureq", "2.12.1"),
231 ("walkdir", "2.5.0"),
232 ("wild", "2.2.1"),
233];
234
235/// `true` when `resolved` is a version this crate's rules were NOT written against — i.e. strictly
236/// newer than its ceiling. Unknown crate, unparseable version, or an exact match: `false`, keep the
237/// exemption. Comparison is numeric per dotted component so `1.10.0 > 1.9.0`; a pre-release or build
238/// suffix on the resolved version is treated as ABOVE the release it decorates, which is the
239/// charging direction.
240/// Version-only comparison: is `a` strictly newer than `b`? Shared with `candor-scan`'s lockfile
241/// parser so "which of two versions of one crate do we keep" is answered by the SAME comparator that
242/// decides whether a version is beyond review — one authority, not two spellings that drift.
243pub fn calibration_exceeded_raw(a: &str, b: &str) -> bool {
244 let parts = |v: &str| -> Vec<u64> {
245 v.split(['+', '-']).next().unwrap_or("")
246 .split('.')
247 .map(|p| p.parse::<u64>().unwrap_or(0))
248 .collect()
249 };
250 let (x, y) = (parts(a), parts(b));
251 for i in 0..x.len().max(y.len()) {
252 let (p, q) = (x.get(i).copied().unwrap_or(0), y.get(i).copied().unwrap_or(0));
253 if p != q {
254 return p > q;
255 }
256 }
257 a != b
258}
259
260pub fn calibration_exceeded(crate_name: &str, resolved: &str) -> bool {
261 let Some((_, ceiling)) = CALIBRATED_CEILINGS.iter().find(|(c, _)| *c == crate_name) else {
262 return false;
263 };
264 let parts = |v: &str| -> Vec<u64> {
265 v.split(['+', '-']).next().unwrap_or("")
266 .split('.')
267 .map(|p| p.parse::<u64>().unwrap_or(0))
268 .collect()
269 };
270 let (r, c) = (parts(resolved), parts(ceiling));
271 if r.is_empty() {
272 return false;
273 }
274 for i in 0..r.len().max(c.len()) {
275 let (a, b) = (r.get(i).copied().unwrap_or(0), c.get(i).copied().unwrap_or(0));
276 if a != b {
277 return a > b;
278 }
279 }
280 // Equal numerically (trailing zero components are padding: `0.39` == `0.39.0`). A pre-release/build
281 // suffix means it is not the reviewed artifact, so the SUFFIXES must agree — comparing the whole strings
282 // read the two-component ceiling `("rusqlite", "0.39")` as exceeded by the very release it was reviewed
283 // against (`0.39.0`) and withheld the exemption (SOUNDNESS R1528; pinned by a test until the reachable
284 // rusqlite rows had rules).
285 let suffix = |v: &str| -> String { v.find(['+', '-']).map(|i| v[i..].to_string()).unwrap_or_default() };
286 suffix(resolved) != suffix(ceiling)
287}
288
289
290/// Crates REVIEWED AND FOUND TO PERFORM NO EFFECT OF THEIR OWN — the κ ledger treats them as covered, so
291/// their calls stop being disclosed blind spots.
292///
293/// SEPARATE FROM `CALIBRATED_CRATES` BY NECESSITY, not taste. That list means "classify has effect rules
294/// here", and `calibrated_crates_are_live` fails any entry no rule matches — "a dead entry would silently
295/// suppress a real coverage warning". A genuinely pure crate has no rule to be live, so it cannot go
296/// there; without this list the only way to silence its noise would be to invent a rule, which is worse.
297///
298/// **THIS LIST MANUFACTURES PURITY CLAIMS, so an entry needs evidence and not a reputation.** A crate here
299/// stops being disclosed and starts being believed. Each of these was checked against its source in the
300/// local cargo registry for `std::{fs,net,process,env}` and stdio use, and every apparent hit was a DOC
301/// COMMENT (serde_json's `/// [`File`]: std::fs::File`, serde_yml's `/// io::stdout()`):
302///
303/// serde_json 1.0.151, serde_yml 0.0.12, toml 1.1.3, regex 1.13.1, sha2 0.11.0
304///
305/// `color_eyre` was on the same filing and is NOT here — FETCHED AND CHECKED 2026-08-03, and it is not
306/// pure. It reads `RUST_BACKTRACE` / `RUST_LIB_BACKTRACE` / `RUST_SPANTRACE` / `COLORBT_SHOW_HIDDEN`
307/// (Env, `config.rs:939..1175`) and **opens source files to render code snippets** (Fs,
308/// `config.rs:248`). It is also not CALIBRATED, deliberately: the `File::open` sits inside
309/// `impl fmt::Display for SourceSection`, reached when a report is RENDERED rather than through any named
310/// verb a caller invokes — so there is no path for a rule to match, and calibrating the crate would turn
311/// that render path into an unmatched path, i.e. a PURITY CLAIM over the file read. Its calls therefore
312/// stay disclosed as a blind spot. The noise is real; it is also honest, and that is the right trade.
313///
314/// THE SERIALIZER CAVEAT, worth stating because it is the one that looks wrong: `serde_json::from_reader`
315/// and `to_writer` do move bytes — but through a handle the CALLER had to obtain, and obtaining it (a
316/// `File::open`, a `TcpStream::connect`) is already classified on the caller. The crate performs no
317/// syscall of its own, so charging it would double-count an effect the caller already carries.
318pub const REVIEWED_PURE_CRATES: [&str; 5] = ["serde_json", "serde_yml", "toml", "regex", "sha2"];
319
320/// The completeness gate's escape hatch (`eval/coverage-gate/`, `tests/coverage_gate.rs`): exact
321/// (crate, consumer-facing path) pairs a human has read and found genuinely pure, despite `candor-scan`
322/// self-scanning the crate's own vendored source finding a reachable Fs/Net/Db/Exec effect there. SEPARATE
323/// from `REVIEWED_PURE_CRATES` — that list exempts a WHOLE crate from the coverage LEDGER's crate-level
324/// disclosure; this one exempts a single ENTRY from the completeness GATE's per-function differential.
325/// The gate's generator run (2026-08-27) found 669 self-scan-confirmed entries `classify()` already
326/// recognizes (checked in as `covered.tsv`, asserted every push) and 251 it did not yet recognize under
327/// any guessed spelling (checked in as `open.tsv`, a ratchet — NOT individually hand-verified, see that
328/// file's header). An `open.tsv` row belongs here ONLY once read against the crate's real source and
329/// confirmed to perform no effect (the SAME evidence bar `REVIEWED_PURE_CRATES` documents) — until then
330/// it stays in the ratchet as an open question, not a silent assumption. The five below were the first
331/// batch, from the 2026-08-27 coverage-gate triage:
332///
333/// - `curl::Multi::timeout` — this file's own `curl` rule (above) already documents WHY: `Easy::
334/// timeout` is a pure `CURLOPT_TIMEOUT` setter sharing the `::timeout` leaf, and an under-report on
335/// the rare event-loop kick beats mis-tagging every consumer that sets a timeout.
336/// - `execute::command` / `execute::shell` — this file's own `execute` rule (above) already documents
337/// WHY: both free functions only BUILD a `std::process::Command` (no `.spawn()`/`.output()`/
338/// `.status()` call in either body) and return it to the caller, who spawns it themselves — the
339/// caller's own `Command::spawn()` carries the effect. Self-scan's raw "Exec" signal comes from
340/// `std::process::Command::new`'s coarse whole-type rule (constructing a `Command` is Exec by
341/// default, narrowed only for crates like `async_process` that document their own pure-setter
342/// surface), not from an actual subprocess launch in either function.
343/// - `elasticsearch::Response::content_type` — a pure accessor reading a header off an ALREADY-received
344/// response (`self.response.headers().get(..)`, http/response.rs:61) — no I/O of its own. Self-scan's
345/// raw "Net" signal is the underlying HTTP client response type's coarse whole-type propagation, not
346/// a second network round-trip.
347/// - `rusqlite::Context::get_connection` — this file's own rusqlite rule (above) already documents WHY:
348/// it hands back a `ConnectionRef` BORROWED from the ALREADY-established connection a running SQL
349/// function executes within (`ffi::sqlite3_context_db_handle`, not a syscall) — the same "handle
350/// accessor, not a syscall" shape as `TcpStream::local_addr`.
351///
352/// The next two (2026-08-28, the widened-CORE audit's `Ipc` batch — see `eval/coverage-gate/generate.py`
353/// for why `Ipc` only entered the trigger set that day) are a DIFFERENT reason than the five above: not
354/// "no effect", but genuinely UNREACHABLE — the item really does perform the effect self-scan found, but
355/// no external consumer can ever name it, because the module it lives in is declared with no `pub` at
356/// all and neither the module nor the item is re-exported anywhere in the crate root (checked against
357/// real source, both ways):
358/// - `dialoguer::Paging::render_prompt` — `dialoguer::paging` is a bare `mod paging;` (dialoguer
359/// 0.12.0, lib.rs:62: `use paging::Paging;`, not `pub use`), so `pub struct Paging` and its `pub fn
360/// render_prompt` (which DOES call `Term::flush`, a real Ipc effect per this file's own `console`
361/// rule) are internal machinery every `Select`/`MultiSelect`-style prompt uses to render its own
362/// paginated view — the effect is already charged at THOSE public entry points' own `::interact*`
363/// rule (above), not double-counted here.
364/// - `mysql::Stream::connect_socket` — `mysql::io` is a bare `mod io;` (mysql 28.0.0, lib.rs:897, no
365/// `pub use` of `io::*` or `Stream` anywhere), so `pub enum Stream` and its `pub fn connect_socket`
366/// (a real Unix-domain-socket `connect()`, Ipc) are reachable only from within the crate's own
367/// connection-establishment code — a real consumer can't name `mysql::Stream` at all, let alone call
368/// a bare constructor on it.
369pub const REVIEWED_PURE_ENTRIES: &[(&str, &str)] = &[
370 ("curl", "curl::Multi::timeout"),
371 ("execute", "execute::command"),
372 ("execute", "execute::shell"),
373 ("elasticsearch", "elasticsearch::Response::content_type"),
374 ("rusqlite", "rusqlite::Context::get_connection"),
375 ("dialoguer", "dialoguer::Paging::render_prompt"),
376 ("mysql", "mysql::Stream::connect_socket"),
377 // SOUNDNESS R1528 — ureq 3.4.2. Both only ASSEMBLE an agent from parts the caller already holds: a boxed
378 // connector (`boxed_connector`, a one-field wrapper that forwards `connect` and is never invoked here), a
379 // resolver, and a `ConnectionPool::new` whose `Pool::new` copies three numbers out of the `Config`
380 // (agent.rs:112-150, pool.rs:21-27/225-232), plus `SharedCookieJar::new` (an empty in-memory
381 // `cookie_store`). No call opens a socket, reads the environment, touches a file or reads a clock: EXECUTED,
382 // an agent built from a config whose proxy is a live 127.0.0.1 listener made ZERO connections. Self-scan's
383 // `Fs,Net,Unknown` is the dispatch union over every `Connector` implementor reached through the
384 // `Box<dyn Connector>` field, which is never called in these bodies. (`Agent::new_with_defaults` is NOT
385 // here: it builds `Config::default()`, which reads `HTTP_PROXY`/`ALL_PROXY` — it carries an `Env` rule.)
386 ("ureq", "ureq::Agent::new_with_config"),
387 ("ureq", "ureq::Agent::with_parts"),
388 // SOUNDNESS R1528 — chrono 0.4.45, seven rows the self-scan charged `Clock,Env,Fs,Unknown`, which is the
389 // dispatch union over every `TimeZone` implementor reached through a generic bound, including `Local`. Each
390 // body was READ and none reaches a generic zone, the clock, the environment or a file:
391 // * `DateTime::{parse_from_rfc3339, parse_from_rfc2822, parse_from_str, parse_and_remainder}` and
392 // `Parsed::to_datetime` (datetime/mod.rs:1051-1160, format/parsed.rs:903) only parse text (`parse`,
393 // `parse_rfc3339`, `Parsed::to_naive_datetime_with_offset` — no `env::`, `fs::`, `SystemTime` or `Local` in
394 // format/parse.rs or format/parsed.rs) and build the result with the CONCRETE `FixedOffset`
395 // (`FixedOffset::east_opt(..)` then `and_local_timezone(tz)` / `offset.from_local_datetime`, parse.rs:99-104,
396 // parsed.rs:913): `FixedOffset`'s `TimeZone` impl is in-memory arithmetic. EXECUTED: all four return the same
397 // result with `TZ` unset and `TZ=Asia/Tokyo`, offset +05:30 taken from the text.
398 // * `DateTime::fixed_offset` is `with_timezone(&self.offset().fix())` and `DateTime::years_since` compares
399 // calendar fields: both touch only the offset already stored in `self` (`Offset::fix`, whose implementors in
400 // the family — `FixedOffset`, `Utc`, `Local`'s `FixedOffset`, `chrono_tz`'s `TzOffset` — are arithmetic) and
401 // the target zone `FixedOffset`. EXECUTED: a `DateTime<Local>` converts with `fixed_offset` to the same instant
402 // and offset whatever `TZ` says afterwards. The rows that DO depend on the zone (`with_timezone` and its
403 // kin) are filed instead: R1557.
404 ("chrono", "chrono::DateTime::fixed_offset"),
405 ("chrono", "chrono::DateTime::years_since"),
406 ("chrono", "chrono::DateTime::parse_and_remainder"),
407 ("chrono", "chrono::DateTime::parse_from_rfc2822"),
408 ("chrono", "chrono::DateTime::parse_from_rfc3339"),
409 ("chrono", "chrono::DateTime::parse_from_str"),
410 ("chrono", "chrono::Parsed::to_datetime"),
411 // SOUNDNESS R1528 — mysql_async 0.37.1, `StatementLike::to_statement` on `Arc`/`Box`/`Cow`/`String`/`Vec`. Each
412 // body is `to_statement_move` (queryable/stmt.rs:43-68): it BUILDS a boxed `async move` block (parse the named
413 // params, look in the statement cache, `prepare_statement`) and wraps it in `ToStatementResult::Mediate`; nothing
414 // runs until that future is polled. A consumer CANNOT poll it: `ToStatementResult` lives in the private
415 // `queryable::stmt` module and is not re-exported, it has no methods and is not a `Future`, and the library itself
416 // never calls `to_statement` (grep: no other use in the crate; `exec`/`prep` go through `get_statement`). The trait
417 // is nameable only as the prelude's SUB-trait bound (`prelude::StatementLike: queryable::stmt::StatementLike`), so
418 // the call is reachable in a generic body and nowhere else. EXECUTED (fx_mysql, docker mysql:8 behind a counting
419 // proxy): `to_statement` through that bound with a `String`, a `&str` and a `Statement`, result held then dropped =
420 // 0 bytes up/down and Com_stmt_prepare +0, against `Conn::prep` beside it (13 up / 43 down, Com_stmt_prepare +1);
421 // compile errors E0599 (`resolve` not found on the returned enum) and E0603 (module `queryable` is private) for
422 // every attempt to drive it.
423 ("mysql_async", "mysql_async::Arc::to_statement"),
424 ("mysql_async", "mysql_async::Box::to_statement"),
425 ("mysql_async", "mysql_async::Cow::to_statement"),
426 ("mysql_async", "mysql_async::String::to_statement"),
427 ("mysql_async", "mysql_async::Vec::to_statement"),
428 // SOUNDNESS R1528 — sqlx-core 0.9.0, `PoolConnection::{detach, leak}`. Both return the connection's inner
429 // `DB::Connection` after `take_live()` (pool/connection.rs:113-124); `detach` floats it and drops the size guard
430 // (`DecrementSizeGuard::drop`, pool/inner.rs:614: an atomic decrement and a semaphore release). The doc comment's
431 // "a task will be spawned to replace this connection" is NOT in the body. EXECUTED against postgres 16 behind a
432 // counting proxy: both calls moved 0 bytes and opened 0 connections, in the `sqlx` and `sqlx_core` spellings.
433 ("sqlx_core", "sqlx_core::pool::connection::PoolConnection::detach"),
434 ("sqlx_core", "sqlx_core::pool::connection::PoolConnection::leak"),
435 // SOUNDNESS R1528 — redis 1.7.1, `Client::build_with_tls` and `ClusterClientBuilder::build`. The first is
436 // `into_connection_info` plus `inner_build_with_tls` (tls.rs:32), which `retrieve_tls_certificates` parses from
437 // the PEM BYTES the caller put in `TlsCertificates` (`CertificateDer::pem_slice_iter`, in memory) and stores in a
438 // `Client { connection_info }`; the second assembles `ClusterParams` the same way and opens no socket
439 // (cluster_handling/client.rs:296). The generator's `Fs` came from the `pem` vocabulary, not a file read: neither
440 // body names `std::fs`, a path, or `rustls_native_certs`. EXECUTED: `build_with_tls("rediss://..")` moved 0
441 // commands and 0 connections on the server.
442 ("redis", "redis::client::Client::build_with_tls"),
443 ("redis", "redis::cluster_handling::client::ClusterClientBuilder::build"),
444 // SOUNDNESS R1528 — lettre 0.11.23, `SmtpTransport::unencrypted_localhost` and `AsyncSmtpTransport::unencrypted_localhost`.
445 // Both are `builder_dangerous("localhost").build()` with NO argument, so the connection pool is always the default
446 // (`PoolConfig::min_idle` 0): the maintenance thread (sync) / task (async) loops over `count..0` and has nothing to
447 // connect (pool/sync_impl.rs:60-83, async_impl.rs:55). EXECUTED, the transport HELD 1.5 s: no `connect` logged, 0
448 // connections through the counting proxy, sync and async. The self-scan's `Net` is the loop body's `client.connection()`
449 // that `min_idle` can never reach from these two entries. (`SmtpTransportBuilder::build` is NOT pure: a caller's
450 // `pool_config(.min_idle(1))` makes it connect, EXECUTED, R1592.)
451 ("lettre", "lettre::transport::smtp::transport::SmtpTransport::unencrypted_localhost"),
452 ("lettre", "lettre::transport::smtp::async_transport::AsyncSmtpTransport::unencrypted_localhost"),
453 // SOUNDNESS R1528 — lapin 4.12.0, `Channel::wait_for_confirms`. It drains the returned-messages queue and awaits the last
454 // pending publisher confirm, a promise the connection's IO loop thread resolves (channel.rs:324-332); it writes no frame.
455 // EXECUTED against RabbitMQ behind a counting proxy: 0 bytes UP both with nothing pending and with a publish pending (the
456 // 21 bytes DOWN in the second case are the confirmation, read by the IO loop that `connect` started, charged there; the
457 // `basic_publish` carries the `Net`).
458 ("lapin", "lapin::channel::Channel::wait_for_confirms"),
459 // SOUNDNESS R1528 (b10) — mysql 28.0.3, `PooledConn::unwrap`: `self.conn.take().unwrap()`, it moves the `Conn` out of the pool wrapper
460 // (conn/pool/mod.rs:272). EXECUTED against MySQL 8 behind a counting proxy: 0 connections and 0 bytes in either direction. The
461 // `Fs` and `Unknown` the self-scan reports are the drop glue of the consumed wrapper, which holds no connection by then.
462 ("mysql", "mysql::conn::pool::PooledConn::unwrap"),
463 // SOUNDNESS R1528 (b10) — reqwest 0.13.5, `PoolClient::send_request` (the HTTP/3 pool client): it really does dispatch, but
464 // `mod async_impl;` is a bare private module (lib.rs:372) whose only re-exports are `Body, Client, ClientBuilder, Request,
465 // RequestBuilder, Response, Upgraded` and `multipart`, so no consumer can name `PoolClient` at all — the same shape as
466 // `mysql::Stream::connect_socket` above. The reachable HTTP/3 dispatch is `Client::{execute,send}`, already `Net`. (The comment
467 // above `reqwest::multipart::Part::file` already said this row was "removed from open.tsv rather than guessed"; it never was.)
468 ("reqwest", "reqwest::async_impl::h3_client::pool::PoolClient::send_request"),
469];
470
471/// SOUNDNESS R1528 step 3 — THE VERDICT REGISTER for `eval/coverage-gate/open.tsv`, keyed on the row's
472/// IDENTITY (`(crate, entry)`, the manifest's last column — not the guessed consumer path, which a rule
473/// change can rewrite).
474///
475/// Every `open.tsv` row is an effectful public function of a calibrated crate that `classify()` names
476/// no effect for — and, within the crate's ceiling, the coverage ledger exempts the call, so a consumer
477/// is told nothing (R1528, EXECUTED). Until 2026-11-08 a row that GREW in the weekly refresh left the red
478/// by being copied into `open.tsv`: 108 rows that way since the 10-08 regeneration, with no rule, no
479/// read and no filed row. **Tom's ruling (2026-11-08): a newly grown row clears the red ONLY with a
480/// recorded verdict, paid for by the engine lane that caused it.** The three verdicts are:
481/// 1. a `classify()` rule — the row then leaves `open.tsv` for `covered.tsv` on regeneration;
482/// 2. read-and-pure — an entry in `REVIEWED_PURE_ENTRIES` above, at that list's evidence bar;
483/// 3. a FILED SOUNDNESS row — an entry HERE: `(crate, entry, "R<id>:<kind>")`, where `<kind>` is
484/// `silent` (a real effect a consumer is not told about; rule pending), `overcharge` (the self-scan
485/// oracle fabricates the effect — a scanner row, R1420(a)'s class), or `conditional` (the effect
486/// depends on a type argument or a configuration the caller chooses; the row says which).
487///
488/// The rows that were already in `open.tsv` when the ruling was made are the pre-ruling backlog,
489/// tracked as a population by R1528 itself and listed in `eval/coverage-gate/grandfathered.tsv`, which
490/// may only shrink. Enforced twice, on purpose: `tests/coverage_gate.rs` on every push (a row in
491/// `open.tsv` that is neither grandfathered nor here fails — closing the splice route), and the weekly
492/// `generate.py --diff-manifests` (a grown row without a verdict fails the job; one with a verdict does
493/// not). A verdict is DATA the gate checks, not prose: a malformed kind, an unknown crate or a verdict
494/// for a row that is not in `open.tsv` fails the push-time test.
495/// ureq 3.4.2 (SOUNDNESS R1528, ureq lane). R1553: every `ConfigBuilder<S>` setter and `RequestScope::config`
496/// reach `Env` only through `RequestBuilder::request_level_config` (request.rs:405-413), which builds
497/// `Config::default()` — and so reads `HTTP_PROXY`/`ALL_PROXY` — only when the builder already holds a URL
498/// error; `AgentScope`, `HttpCrateScope` and `RequestExtScope` setters are pure, and the chain's own start
499/// (`ureq::get`, `Config::builder`, `Agent::new_with_defaults`) already carries the unconditional read.
500/// R1554: `Either`/`Box<dyn Transport>`/`ChainedConnector` forward to the transport or connectors they wrap,
501/// so the effect is the type argument's: EXECUTED `Net` over a TCP transport, and a caller-supplied in-memory
502/// transport has none. R1555: the nine free verbs only build a request; the unreported effect is the `Env`
503/// read in `Config::default` (Net is charged by candor-scan's scan-only builder-entry table, by design).
504pub const OPEN_ENTRY_VERDICTS: &[(&str, &str, &str)] = &[
505 ("ureq", "ureq::unversioned::transport::Box::await_input", "R1554:conditional"),
506 ("ureq", "ureq::unversioned::transport::Box::is_open", "R1554:conditional"),
507 ("ureq", "ureq::unversioned::transport::Box::transmit_output", "R1554:conditional"),
508 ("ureq", "ureq::unversioned::transport::chain::ChainedConnector::connect", "R1554:conditional"),
509 ("ureq", "ureq::config::ConfigBuilder::accept", "R1553:conditional"),
510 ("ureq", "ureq::config::ConfigBuilder::accept_encoding", "R1553:conditional"),
511 ("ureq", "ureq::config::ConfigBuilder::allow_non_standard_methods", "R1553:conditional"),
512 ("ureq", "ureq::config::ConfigBuilder::http_status_as_error", "R1553:conditional"),
513 ("ureq", "ureq::config::ConfigBuilder::https_only", "R1553:conditional"),
514 ("ureq", "ureq::config::ConfigBuilder::input_buffer_size", "R1553:conditional"),
515 ("ureq", "ureq::config::ConfigBuilder::ip_family", "R1553:conditional"),
516 ("ureq", "ureq::config::ConfigBuilder::max_idle_age", "R1553:conditional"),
517 ("ureq", "ureq::config::ConfigBuilder::max_idle_connections", "R1553:conditional"),
518 ("ureq", "ureq::config::ConfigBuilder::max_idle_connections_per_host", "R1553:conditional"),
519 ("ureq", "ureq::config::ConfigBuilder::max_redirects", "R1553:conditional"),
520 ("ureq", "ureq::config::ConfigBuilder::max_redirects_will_error", "R1553:conditional"),
521 ("ureq", "ureq::config::ConfigBuilder::max_response_header_size", "R1553:conditional"),
522 ("ureq", "ureq::config::ConfigBuilder::middleware", "R1553:conditional"),
523 ("ureq", "ureq::config::ConfigBuilder::no_delay", "R1553:conditional"),
524 ("ureq", "ureq::config::ConfigBuilder::output_buffer_size", "R1553:conditional"),
525 ("ureq", "ureq::config::ConfigBuilder::proxy", "R1553:conditional"),
526 ("ureq", "ureq::config::ConfigBuilder::redirect_auth_headers", "R1553:conditional"),
527 ("ureq", "ureq::config::ConfigBuilder::save_redirect_history", "R1553:conditional"),
528 ("ureq", "ureq::config::ConfigBuilder::timeout_await_100", "R1553:conditional"),
529 ("ureq", "ureq::config::ConfigBuilder::timeout_connect", "R1553:conditional"),
530 ("ureq", "ureq::config::ConfigBuilder::timeout_global", "R1553:conditional"),
531 ("ureq", "ureq::config::ConfigBuilder::timeout_per_call", "R1553:conditional"),
532 ("ureq", "ureq::config::ConfigBuilder::timeout_recv_body", "R1553:conditional"),
533 ("ureq", "ureq::config::ConfigBuilder::timeout_recv_response", "R1553:conditional"),
534 ("ureq", "ureq::config::ConfigBuilder::timeout_resolve", "R1553:conditional"),
535 ("ureq", "ureq::config::ConfigBuilder::timeout_send_body", "R1553:conditional"),
536 ("ureq", "ureq::config::ConfigBuilder::timeout_send_request", "R1553:conditional"),
537 ("ureq", "ureq::config::ConfigBuilder::tls_config", "R1553:conditional"),
538 ("ureq", "ureq::config::ConfigBuilder::user_agent", "R1553:conditional"),
539 ("ureq", "ureq::unversioned::transport::chain::Either::await_input", "R1554:conditional"),
540 ("ureq", "ureq::unversioned::transport::chain::Either::is_open", "R1554:conditional"),
541 ("ureq", "ureq::unversioned::transport::chain::Either::transmit_output", "R1554:conditional"),
542 ("ureq", "ureq::config::typestate::RequestScope::config", "R1553:conditional"),
543 ("ureq", "ureq::connect", "R1555:silent"),
544 ("ureq", "ureq::delete", "R1555:silent"),
545 ("ureq", "ureq::get", "R1555:silent"),
546 ("ureq", "ureq::head", "R1555:silent"),
547 ("ureq", "ureq::options", "R1555:silent"),
548 ("ureq", "ureq::patch", "R1555:silent"),
549 ("ureq", "ureq::post", "R1555:silent"),
550 ("ureq", "ureq::put", "R1555:silent"),
551 ("ureq", "ureq::trace", "R1555:silent"),
552 // chrono 0.4.45, R1557 (SOUNDNESS R1528, clock/env lane): the effect of each row below is the EFFECT OF THE
553 // `TimeZone` TYPE ARGUMENT, which `classify` cannot see. `with_timezone`, `checked_{add,sub}_months`,
554 // `duration_{round,round_up,trunc}` (via `Add<TimeDelta>`), `NaiveDateTime::and_local_timezone` and
555 // `Parsed::to_datetime_with_timezone` end in `tz.from_utc_datetime(..)`/`from_local_datetime(..)`, so with `Utc` or
556 // `FixedOffset` they are arithmetic and with `Local` they read `TZ`, `/etc/localtime` and the wall clock (the same
557 // reads the `Local::offset_from_*` rules charge). EXECUTED (fx_chrono `tz_env`/`cond`): `utc.with_timezone(&Local)`
558 // gave +01:00, +09:00, -04:00 for TZ unset/Asia/Tokyo/America/New_York; the same call with `&FixedOffset` ignored TZ.
559 // A rule would charge every `Utc` consumer (fabrication), so the consumer stays silent: a type-argument locator.
560 // tracing-subscriber 0.3.23, R1558: `Layer::on_{event,new_span,enter,exit,close}` over `Layered`, `Filtered`,
561 // `Option<L>` and `fmt::Layer`, `Context::event` and `Format::format_event` forward to the layer, subscriber,
562 // timer (`T: FormatTime`) or writer (`W: MakeWriter`) they wrap. EXECUTED (fx_ts `fwd`/`fmtevent`): a wrapper around
563 // `fmt::layer()` printed "<timestamp> INFO ..: ..." to stdout (Log + Clock); the same wrapper's `Option<Counter>`
564 // printed nothing and read no clock; `Format<Full, ()>` (`without_time`) printed no timestamp. `Layer::on_event` is
565 // also the TRAIT method every layer shares (consumer path `tracing_subscriber::Layer::on_event`), so a rule on it
566 // would charge every pure layer.
567 // jiff 0.2.38, R1559: `*_with(db, ..)` use the caller's `TimeZoneDatabase`, and the disk read happens where that
568 // database was built (`from_env`/`from_dir`/`from_concatenated_path`/`tz::db()` carry `Fs` rules); EXECUTED, the
569 // same `to_time_zone_with` call succeeded over a `from_dir` database and failed over `bundled()` (no disk).
570 // `TimeZoneDatabase::bundled` reaches a `warn!` only under jiff's non-default `logging` feature AND an empty bundle.
571 ("chrono", "chrono::date::Date::with_timezone", "R1557:conditional"),
572 ("chrono", "chrono::datetime::DateTime::checked_add_months", "R1557:conditional"),
573 ("chrono", "chrono::datetime::DateTime::checked_sub_months", "R1557:conditional"),
574 ("chrono", "chrono::round::DateTime::duration_round", "R1557:conditional"),
575 ("chrono", "chrono::round::DateTime::duration_round_up", "R1557:conditional"),
576 ("chrono", "chrono::round::DateTime::duration_trunc", "R1557:conditional"),
577 ("chrono", "chrono::datetime::DateTime::with_timezone", "R1557:conditional"),
578 ("chrono", "chrono::naive::datetime::NaiveDateTime::and_local_timezone", "R1557:conditional"),
579 ("chrono", "chrono::format::parsed::Parsed::to_datetime_with_timezone", "R1557:conditional"),
580 ("tracing_subscriber", "tracing_subscriber::layer::context::Context::event", "R1558:conditional"),
581 ("tracing_subscriber", "tracing_subscriber::filter::layer_filters::Filtered::on_close", "R1558:conditional"),
582 ("tracing_subscriber", "tracing_subscriber::filter::layer_filters::Filtered::on_enter", "R1558:conditional"),
583 ("tracing_subscriber", "tracing_subscriber::filter::layer_filters::Filtered::on_event", "R1558:conditional"),
584 ("tracing_subscriber", "tracing_subscriber::filter::layer_filters::Filtered::on_exit", "R1558:conditional"),
585 ("tracing_subscriber", "tracing_subscriber::filter::layer_filters::Filtered::on_new_span", "R1558:conditional"),
586 ("tracing_subscriber", "tracing_subscriber::fmt::format::json::Format::format_event", "R1558:conditional"),
587 ("tracing_subscriber", "tracing_subscriber::fmt::format::Format::format_event", "R1558:conditional"),
588 ("tracing_subscriber", "tracing_subscriber::fmt::format::pretty::Format::format_event", "R1558:conditional"),
589 ("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::on_close", "R1558:conditional"),
590 ("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::on_enter", "R1558:conditional"),
591 ("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::on_event", "R1558:conditional"),
592 ("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::on_exit", "R1558:conditional"),
593 ("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::on_new_span", "R1558:conditional"),
594 ("tracing_subscriber", "tracing_subscriber::layer::layered::Layered::on_close", "R1558:conditional"),
595 ("tracing_subscriber", "tracing_subscriber::layer::layered::Layered::on_enter", "R1558:conditional"),
596 ("tracing_subscriber", "tracing_subscriber::layer::layered::Layered::on_event", "R1558:conditional"),
597 ("tracing_subscriber", "tracing_subscriber::layer::layered::Layered::on_exit", "R1558:conditional"),
598 ("tracing_subscriber", "tracing_subscriber::layer::layered::Layered::on_new_span", "R1558:conditional"),
599 ("tracing_subscriber", "tracing_subscriber::layer::Option::on_close", "R1558:conditional"),
600 ("tracing_subscriber", "tracing_subscriber::layer::Option::on_enter", "R1558:conditional"),
601 ("tracing_subscriber", "tracing_subscriber::layer::Option::on_event", "R1558:conditional"),
602 ("tracing_subscriber", "tracing_subscriber::layer::Option::on_exit", "R1558:conditional"),
603 ("tracing_subscriber", "tracing_subscriber::layer::Option::on_new_span", "R1558:conditional"),
604 ("jiff", "jiff::fmt::strtime::BrokenDownTime::to_zoned_with", "R1559:conditional"),
605 ("jiff", "jiff::fmt::temporal::pieces::Pieces::to_time_zone_with", "R1559:conditional"),
606 ("jiff", "jiff::fmt::temporal::pieces::TimeZoneAnnotation::to_time_zone_with", "R1559:conditional"),
607 ("jiff", "jiff::tz::db::TimeZoneDatabase::bundled", "R1559:conditional"),
608 // sea-orm 2.0.4, R1570 (SOUNDNESS R1528, sea_orm lane): the six `LoaderTrait` methods on `Vec<M>` perform a real
609 // SELECT per call (query/loader.rs:337-430 forward to the `&[M]` impl, which runs `select(..).all(db)`; EXECUTED:
610 // `load_one`, `load_many`, `load_self`, `load_self_many`, `load_self_via`, `load_self_via_rev` each issued a
611 // `sea_orm.query_all` and returned the related rows), but a consumer's `models.load_many(Entity, db)` has a std
612 // `Vec` receiver and a method that lives in an UNSCANNED dependency, so no path reaches `classify` at all (a
613 // debug trace of every `sea_orm` path the scan asked about holds no `Vec::load_*`), and R1540's std-receiver
614 // edge needs the method's definition in the scan or in a chained report. Measured both arms: `deny Db`,
615 // `deny Log`, `deny Unknown` all exit 0 over a function whose only call is `ms.load_many(..)`; the lockless arm
616 // drops the function from `functions[]`. No rule can fix it (there is no path to match), so the row stays open.
617 // R1571: the six `RusqliteSharedConnection` rows are not reachable by name. In the crates.io `sea-orm` 2.0.4 the
618 // `rusqlite` feature is an empty stub (`rusqlite = []`, no `sea-query-rusqlite` dependency: enabling it fails to
619 // compile, EXECUTED), and in `sea-orm-sync` 2.0.4 (lib name `sea_orm`) the module is `pub(crate)`, so the type can
620 // only be obtained through `match &db.inner { DatabaseConnectionType::RusqliteSharedConnection(c) => c.ping() }`.
621 // EXECUTED there: `begin` held the connection (a second `acquire` timed out at the 400 ms deadline), `ping` ran
622 // SELECT 1, `close`/`close_by_ref` took the open file descriptors on the database from 4 to 0 and made later calls
623 // fail with `Connection closed`. The variant-payload binding has no type, so no path reaches `classify`: all six
624 // functions read pure with NO row and NO `invisible` in both arms (the lock lists `sea-orm-sync`, which the
625 // ceiling table does not match), so a classify rule would be dead code claiming coverage.
626 ("sea_orm", "sea_orm::query::loader::Vec::load_many", "R1570:silent"),
627 ("sea_orm", "sea_orm::query::loader::Vec::load_one", "R1570:silent"),
628 ("sea_orm", "sea_orm::query::loader::Vec::load_self", "R1570:silent"),
629 ("sea_orm", "sea_orm::query::loader::Vec::load_self_many", "R1570:silent"),
630 ("sea_orm", "sea_orm::query::loader::Vec::load_self_via", "R1570:silent"),
631 ("sea_orm", "sea_orm::query::loader::Vec::load_self_via_rev", "R1570:silent"),
632 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::acquire", "R1571:silent"),
633 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::begin", "R1571:silent"),
634 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::close", "R1571:silent"),
635 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::close_by_ref", "R1571:silent"),
636 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::ping", "R1571:silent"),
637 ("sea_orm", "sea_orm::driver::rusqlite::RusqliteSharedConnection::transaction", "R1571:silent"),
638 // async-nats 0.50.0, R1576 (SOUNDNESS R1528, async_nats lane): `jetstream::{new, with_domain, with_prefix}` and
639 // `ContextBuilder::build` share one body that opens NO connection (EXECUTED against a nats-server: zero messages,
640 // zero bytes, zero subscriptions) and ends in `spawn_acker`, whose task logs `debug!("Acker task exited")` when the
641 // `Context` is dropped (EXECUTED with a tracing layer). So the rows' own effect is a `Log` and a rule would be
642 // true. It is not added: the engine does not type `let js = jetstream::new(c)` (a dependency's free fn has no
643 // `ReturnIndex` entry), so every `js.<verb>()` after it (`query_account`, `get_stream`, `get_key_value`...) reaches
644 // no classify path and its `Net` is unreported. MEASURED both arms on `let js = jetstream::new(c);
645 // js.query_account().await`: no rule, `invisible:[async_nats]` (lock) / absent (lockless); with a `Log` rule,
646 // `['Log']` alone and the `invisible` disclosure GONE, `deny Net` exit 0 either way. A rule would delete the one
647 // accidental disclosure of a `Net` it cannot supply. The annotated spelling `let js: Context = ...` does classify.
648 ("async_nats", "async_nats::jetstream::new", "R1576:silent"),
649 ("async_nats", "async_nats::jetstream::with_domain", "R1576:silent"),
650 ("async_nats", "async_nats::jetstream::with_prefix", "R1576:silent"),
651 ("async_nats", "async_nats::jetstream::context::ContextBuilder::build", "R1576:silent"),
652 // rdkafka 0.39.0, SOUNDNESS R1528 (rdkafka lane). Every row below was EXECUTED against a real Kafka broker
653 // (docker `apache/kafka` behind a counting TCP proxy) and the 45 other grandfathered rows were given rules
654 // (`rdkafka_r1528`).
655 // R1581: `FutureProducerContext::{log, error, stats, stats_raw}` forward to the WRAPPED context
656 // (producer/future_producer.rs:157-171), so the effect is the type argument's. EXECUTED with
657 // `DefaultClientContext`: the `log` callback re-emits librdkafka's records through the `log` crate (hundreds of
658 // `Log` events across a `FutureProducer`'s life); a caller-supplied context that records nothing has none.
659 // R1582: the rows whose ONLY effect is the `trace!("Destroying {}: {:?}", ..)` / `trace!("Destroyed ..")` pair
660 // that `NativePtr::drop` (util.rs:331-335) emits when a librdkafka handle is dropped. EXECUTED with a Trace-level
661 // `log` logger: two `rdkafka`-target records per dropped `TopicPartitionList`, `OwnedHeaders`, `OwnedMessage`
662 // detach and so on, and ZERO bytes through the broker proxy on a quiet client for every one of them
663 // (`assignment`, `subscription`, `position`, `incremental_unassign` and `begin_transaction` are local state;
664 // `TopicPartitionList::{new, with_capacity, from_topic_map}`, `OwnedHeaders::{new, new_with_capacity}`,
665 // `{Borrowed,}Headers::detach`, `BorrowedMessage::detach` and `ClientConfig::create_native_config` build or copy
666 // a value). The `Log` is real, Trace-level, and lands at the DROP, wherever the value dies — a per-constructor
667 // rule cannot say that, and charging only these rows would leave every other handle type (`BorrowedMessage`,
668 // `Metadata`, `NativeClientConfig`, the `Err` path of `RDKafkaError`) silent on the same trace. So it is filed,
669 // not ruled: a consumer's `deny Log` is not told. Severity floor: Trace level, no I/O.
670 // R1583: `BaseConsumer::close_queue` only ENQUEUES the termination (EXECUTED on a live group member: 0 bytes up
671 // from the call, 76 down; the LeaveGroup and heartbeat go out during the `poll` that serves the queue, 120 bytes
672 // up in the next three seconds). The wire effect belongs to the serving `poll` (already `Net`) or to the
673 // consumer's drop, not to this call.
674 ("rdkafka", "rdkafka::consumer::base_consumer::BaseConsumer::assignment", "R1582:silent"),
675 ("rdkafka", "rdkafka::consumer::base_consumer::BaseConsumer::close_queue", "R1583:conditional"),
676 ("rdkafka", "rdkafka::consumer::base_consumer::BaseConsumer::incremental_unassign", "R1582:silent"),
677 ("rdkafka", "rdkafka::consumer::base_consumer::BaseConsumer::position", "R1582:silent"),
678 ("rdkafka", "rdkafka::consumer::base_consumer::BaseConsumer::subscription", "R1582:silent"),
679 ("rdkafka", "rdkafka::producer::base_producer::BaseProducer::begin_transaction", "R1582:silent"),
680 ("rdkafka", "rdkafka::message::BorrowedHeaders::detach", "R1582:silent"),
681 ("rdkafka", "rdkafka::message::BorrowedMessage::detach", "R1582:silent"),
682 ("rdkafka", "rdkafka::config::ClientConfig::create_native_config", "R1582:silent"),
683 ("rdkafka", "rdkafka::producer::future_producer::FutureProducer::begin_transaction", "R1582:silent"),
684 ("rdkafka", "rdkafka::producer::future_producer::FutureProducerContext::error", "R1581:conditional"),
685 ("rdkafka", "rdkafka::producer::future_producer::FutureProducerContext::log", "R1581:conditional"),
686 ("rdkafka", "rdkafka::producer::future_producer::FutureProducerContext::stats", "R1581:conditional"),
687 ("rdkafka", "rdkafka::producer::future_producer::FutureProducerContext::stats_raw", "R1581:conditional"),
688 ("rdkafka", "rdkafka::message::OwnedHeaders::new", "R1582:silent"),
689 ("rdkafka", "rdkafka::message::OwnedHeaders::new_with_capacity", "R1582:silent"),
690 ("rdkafka", "rdkafka::consumer::stream_consumer::StreamConsumer::assignment", "R1582:silent"),
691 ("rdkafka", "rdkafka::consumer::stream_consumer::StreamConsumer::incremental_unassign", "R1582:silent"),
692 ("rdkafka", "rdkafka::consumer::stream_consumer::StreamConsumer::position", "R1582:silent"),
693 ("rdkafka", "rdkafka::consumer::stream_consumer::StreamConsumer::subscription", "R1582:silent"),
694 ("rdkafka", "rdkafka::producer::base_producer::ThreadedProducer::begin_transaction", "R1582:silent"),
695 ("rdkafka", "rdkafka::topic_partition_list::TopicPartitionList::from_topic_map", "R1582:silent"),
696 ("rdkafka", "rdkafka::topic_partition_list::TopicPartitionList::new", "R1582:silent"),
697 ("rdkafka", "rdkafka::topic_partition_list::TopicPartitionList::with_capacity", "R1582:silent"),
698 // mongodb 3.9.1 and mysql_async 0.37.1, SOUNDNESS R1528 (dbs lane). 68 other rows of the two crates were given
699 // rules (`mongodb_r1528`, `mysql_async_r1528`), five were read and found pure (`REVIEWED_PURE_ENTRIES`).
700 // R1584: `conditional` on a setting the caller chooses. `AuthMechanism::validate_credential` (and
701 // `ConnectionString::parse`, which calls it) only reaches an output when the `aws-auth` feature is on AND the
702 // mechanism is MONGODB-AWS AND the URI carries credentials (client/auth.rs:195-210: `log_warning!`, which is
703 // `log::warn!` under the `log` feature and a debug-build `eprintln!` otherwise); with default features both are
704 // pure. `ParseConnectionString::execute` (`ClientOptions::parse(..).await`) does the SRV and TXT DNS lookups ONLY
705 // for a `mongodb+srv` scheme (client/options/parse.rs:33-35). EXECUTED: `mongodb://` parses in 0.5 ms and
706 // `mongodb+srv://nonexistent-cluster.invalid` fails with `DNS error: no records found for Query { name:
707 // "_mongodb._tcp.nonexistent-cluster.invalid.", query_type: SRV }`. Not charged `Net`: every `mongodb` consumer calls
708 // it, so a rule would fabricate `Net` for every plain URI.
709 ("mongodb", "mongodb::client::auth::AuthMechanism::validate_credential", "R1584:conditional"),
710 ("mongodb", "mongodb::client::options::ConnectionString::parse", "R1584:conditional"),
711 ("mongodb", "mongodb::client::options::parse::ParseConnectionString::execute", "R1584:conditional"),
712 // R1585: the client-side field-level encryption surface exists only under the `in-use-encryption` feature, which
713 // links libmongocrypt (`mongocrypt-sys` build.rs: `rustc-link-lib=dylib=mongocrypt`, no copy bundled; absent
714 // here), so none of these five could be EXECUTED. Read: `EncryptedClientBuilder::build` is
715 // `Client::with_options` plus `init_csfle` ("May perform DNS lookups and/or spawn mongocryptd", its own doc);
716 // `CreateDataKey::execute` / `Encrypt::execute` are the macro inputs whose creators `ClientEncryption::
717 // {create_data_key, encrypt}` run the same `run_ctx` state machine as the already-`Net` `decrypt`;
718 // `ClientEncryption::new` / `ClientEncryptionBuilder::build` look pure but call `Crypt::builder().build()` (an FFI
719 // object) and build a rayon pool sized by `num_cpus::get()` (cgroup reads on Linux). Filed `silent`, rule pending an
720 // executed fixture, rather than claimed pure from a read that stops at an FFI boundary.
721 ("mongodb", "mongodb::client::csfle::client_encryption::ClientEncryption::new", "R1585:silent"),
722 ("mongodb", "mongodb::client::csfle::client_encryption::ClientEncryptionBuilder::build", "R1585:silent"),
723 ("mongodb", "mongodb::client::csfle::client_encryption::create_data_key::CreateDataKey::execute", "R1585:silent"),
724 ("mongodb", "mongodb::client::csfle::client_encryption::encrypt::Encrypt::execute", "R1585:silent"),
725 ("mongodb", "mongodb::client::csfle::client_builder::EncryptedClientBuilder::build", "R1585:silent"),
726 // R1586: no spelling of the row can reach `classify`, so no rule can fire and a rule here would only hide the gap.
727 // `PathOrBuf::read` (opts/mod.rs:153) is `tokio::fs::read` of a `Path` variant. EXECUTED: the same call returns
728 // the file's bytes for an existing path and `No such file or directory (os error 2)` for a missing one. The type
729 // is not nameable (`mysql_async::PathOrBuf` does not resolve, E0425) and is reached only as a value out of
730 // `SslOpts::root_certs()`, whose return the scan does not type (the trace shows `SslOpts::root_certs`,
731 // `SslOpts::first`, and nothing for `read`), so every consumer is silent on a real file read.
732 ("mysql_async", "mysql_async::opts::PathOrBuf::read", "R1586:silent"),
733 // R1586, second half: `QueryResult::map` is a real `Db` (EXECUTED: 29.6 MB read) but the bare name is also what the
734 // scan attributes to every `.map(..)` chained off a `QueryResult` accessor (`r.columns().map(..)` is `Option::map`
735 // and traces as `mysql_async::QueryResult::map`), so a rule would fabricate `Db` on a pure idiom. Filed, not ruled.
736 ("mysql_async", "mysql_async::queryable::query_result::QueryResult::map", "R1586:silent"),
737 // aws-config 1.12.0, R1588. `RegionProviderChain::region` and `Box<dyn ProvideRegion>::region` forward to whichever
738 // providers the chain holds: EXECUTED, `RegionProviderChain::default_provider().region()` read the AWS_* variables and
739 // both profile files and (no region configured) asked the IMDS mock, while `first_try(Region::new(..)).region()` and a
740 // boxed `Region` read nothing. A rule would charge the literal-only chain; the default chain's effect is the chain
741 // BUILDER's, which is not a row.
742 ("aws_config", "aws_config::meta::region::Box::region", "R1588:conditional"),
743 ("aws_config", "aws_config::meta::region::RegionProviderChain::region", "R1588:conditional"),
744 // R1588, second half: `profile::{credentials, token}::Builder::build` only CONSTRUCT a provider (credentials: a
745 // `tracing::debug_span!` and one `AWS_EXECUTION_ENV` read, EXECUTED; token: nothing). The profile files, IMDS and ECS
746 // are touched by the later `provide_credentials` / `provide_token`, which carries no rule. A `Log` or `Env` rule here
747 // would delete, above the 1.12.0 ceiling, the `invisible:[aws_config]` that is today's only disclosure of that I/O.
748 ("aws_config", "aws_config::profile::credentials::Builder::build", "R1588:silent"),
749 ("aws_config", "aws_config::profile::token::Builder::build", "R1588:silent"),
750 // sqlx-core 0.9.0, R1589. `connect_lazy{,_with}` open nothing, EXECUTED in five spellings (0 connections, 0 bytes in a
751 // held two-second window) EXCEPT `min_connections(1)` with `idle_timeout(None).max_lifetime(None)`, where the spawned
752 // maintenance task opened 1 connection and sent 411 bytes (pool/inner.rs:517-524). A rule would charge every lazy pool.
753 ("sqlx_core", "sqlx_core::pool::Pool::connect_lazy", "R1589:conditional"),
754 ("sqlx_core", "sqlx_core::pool::Pool::connect_lazy_with", "R1589:conditional"),
755 ("sqlx_core", "sqlx_core::pool::options::PoolOptions::connect_lazy", "R1589:conditional"),
756 ("sqlx_core", "sqlx_core::pool::options::PoolOptions::connect_lazy_with", "R1589:conditional"),
757 // R1589, second half: the `Box<S: Socket>` forwarders are the socket type's own effect (a TcpStream polls a real
758 // descriptor, a caller's in-memory socket does not). The consumer spelling is the trait path `Socket::poll_*`.
759 ("sqlx_core", "sqlx_core::net::socket::Box::poll_flush", "R1589:conditional"),
760 ("sqlx_core", "sqlx_core::net::socket::Box::poll_read_ready", "R1589:conditional"),
761 ("sqlx_core", "sqlx_core::net::socket::Box::poll_shutdown", "R1589:conditional"),
762 ("sqlx_core", "sqlx_core::net::socket::Box::poll_write_ready", "R1589:conditional"),
763 // R1589, third half: `MigrationSource::resolve` for `PathBuf` and `ResolveWith` reads the migrations directory
764 // (EXECUTED: 1 then 2 migrations as a file is added) but NO spelling reaches it: `p.resolve()` on a `std::path::PathBuf`
765 // receiver and on a `ResolveWith(..)` tuple-struct value (also as a typed parameter) produce no callee path at all.
766 // `Migrator::new` is the consumer-facing verb and is charged `Fs`.
767 ("sqlx_core", "sqlx_core::migrate::source::PathBuf::resolve", "R1589:silent"),
768 ("sqlx_core", "sqlx_core::migrate::source::ResolveWith::resolve", "R1589:silent"),
769 // redis 1.7.1, R1590. `MultiplexedConnection::{new, new_with_config}` write the handshake to the caller-supplied
770 // STREAM: EXECUTED 110 bytes through a TCP proxy to a real server, and the same 110 bytes into an in-memory `duplex`
771 // peer with no server at all. The effect is the stream type's.
772 ("redis", "redis::aio::multiplexed_connection::MultiplexedConnection::new", "R1590:conditional"),
773 ("redis", "redis::aio::multiplexed_connection::MultiplexedConnection::new_with_config", "R1590:conditional"),
774 // R1590, second half: `EntraIdCredentialsProvider::start` spawns a refresh loop over the caller's
775 // `Arc<dyn TokenCredential>` (entra_id.rs:387-432); what it does is that credential's. READ only: no Azure to run it
776 // against (weaker than the rows above).
777 ("redis", "redis::entra_id::EntraIdCredentialsProvider::start", "R1590:conditional"),
778 // R1590, third half: the three Sentinel `build`s draw a random replica index (`rand::rng().random_range`; EXECUTED, 1-2
779 // OS entropy reads on a fresh thread). A `Rand` rule was written and REMOVED: `let mut s = Sentinel::build(u)?;
780 // s.master_for(..)` (and `SentinelClient::build(..)?` then `get_connection()`) leaves `s` untyped, so with the rule the
781 // lock arm reported `['Rand']` and nothing for the `Db` that follows, where before it reported `invisible:[redis]`.
782 ("redis", "redis::sentinel::Sentinel::build", "R1590:silent"),
783 ("redis", "redis::sentinel::SentinelClient::build", "R1590:silent"),
784 ("redis", "redis::sentinel::SentinelClientBuilder::build", "R1590:silent"),
785 // isahc 2.0.1, R1592. `CookieJar::{get_by_name, get_for_uri, set}` read the clock (`Cookie::is_expired`, cookies/cookie.rs:273)
786 // only for a cookie that carries an `expiration`: EXECUTED 1 `clock_gettime` between start and end markers with an
787 // expiring cookie, 0 with an empty jar and 0 with a session cookie, for all three. A `Clock` rule would charge two of the
788 // three shapes of jar the caller can hold. `Response::text` blocks on the response BODY: EXECUTED returning the tail the
789 // server sent 1.5 s after the headers (sync and async), and reading nothing from a response built in memory; the request
790 // that produced the response is charged where it is sent.
791 ("isahc", "isahc::cookies::jar::CookieJar::get_by_name", "R1592:conditional"),
792 ("isahc", "isahc::cookies::jar::CookieJar::get_for_uri", "R1592:conditional"),
793 ("isahc", "isahc::cookies::jar::CookieJar::set", "R1592:conditional"),
794 ("isahc", "isahc::response::Response::text", "R1592:conditional"),
795 // lettre 0.11.23, R1592. `SmtpTransportBuilder::build` and `AsyncSmtpTransportBuilder::build` spawn the pool's maintenance
796 // thread / task, which connects only when the caller asked for `PoolConfig::min_idle(n > 0)`: EXECUTED 0 connections and 0
797 // bytes by default, and 1 connection with 23 bytes up (the EHLO) with `min_idle(1)`, sync and async. `Auth::new_from_response`
798 // emits two `debug!` records (the encoded and the decoded SASL challenge) only when lettre's `tracing` feature is on, which
799 // is off by default: EXECUTED with the feature and a subscriber, 2 records.
800 ("lettre", "lettre::transport::smtp::transport::SmtpTransportBuilder::build", "R1592:conditional"),
801 ("lettre", "lettre::transport::smtp::async_transport::AsyncSmtpTransportBuilder::build", "R1592:conditional"),
802 ("lettre", "lettre::transport::smtp::commands::Auth::new_from_response", "R1592:conditional"),
803 // lapin 4.12.0, R1593. `Connection::connector` runs a CALLER-SUPPLIED connect closure: EXECUTED 1 connection with a closure that
804 // dials the broker, 0 bytes with one that returns an error. `Connection::run` is `drop(self)` then a join on the IO loop:
805 // EXECUTED 0 bytes while another handle (a channel) is alive, but 21 up / 12 down when `self` is the LAST handle, because the
806 // drop closes the connection. `Consumer::set_delegate` runs the caller's delegate on every queued delivery (EXECUTED: it
807 // ran, twice) and moves no byte of its own.
808 ("lapin", "lapin::connection::Connection::connector", "R1593:conditional"),
809 ("lapin", "lapin::connection::Connection::run", "R1593:conditional"),
810 ("lapin", "lapin::consumer::Consumer::set_delegate", "R1593:conditional"),
811 // tonic 0.14.6, R1594. `serve_with_incoming{,_shutdown}` (Server and Router) serve whatever stream they are handed: EXECUTED
812 // over a `TcpIncoming` a client connected and got a reply, and over an in-memory `duplex` stream the request was served with no
813 // `bind`, `listen` or `connect` logged. `Endpoint::tls_config{,_with_verifier}` read `SSL_CERT_FILE` / `SSL_CERT_DIR` and open the
814 // root store only for `ClientTlsConfig::with_native_roots()` (EXECUTED: the bundle was opened with it and not with
815 // `with_webpki_roots()` or a bare config), and read `SSLKEYLOGFILE` and open that file for `use_key_log()` (EXECUTED through
816 // `tls_config_with_verifier`; native roots with a verifier is an error before any read). `Code::from_bytes` and
817 // `Status::from_header_map` log only on malformed input (EXECUTED: 0 records for `"0"` / a valid header map, 1 `trace!` and
818 // 1 `warn!` for the malformed ones). `Streaming::{message, trailers}` read the body they were given: EXECUTED `message` returned
819 // a status the server pushed 1.75 s later and `trailers` was still waiting on a live stream after 1.2 s.
820 ("tonic", "tonic::codec::decode::Streaming::message", "R1594:conditional"),
821 ("tonic", "tonic::codec::decode::Streaming::trailers", "R1594:conditional"),
822 ("tonic", "tonic::status::Code::from_bytes", "R1594:conditional"),
823 ("tonic", "tonic::status::Status::from_header_map", "R1594:conditional"),
824 ("tonic", "tonic::transport::channel::endpoint::Endpoint::tls_config", "R1594:conditional"),
825 ("tonic", "tonic::transport::channel::endpoint::Endpoint::tls_config_with_verifier", "R1594:conditional"),
826 ("tonic", "tonic::transport::server::Router::serve_with_incoming", "R1594:conditional"),
827 ("tonic", "tonic::transport::server::Router::serve_with_incoming_shutdown", "R1594:conditional"),
828 ("tonic", "tonic::transport::server::Server::serve_with_incoming", "R1594:conditional"),
829 ("tonic", "tonic::transport::server::Server::serve_with_incoming_shutdown", "R1594:conditional"),
830 // tempfile 3.27.0 and crossterm 0.29.0, R1595. `TempPath::{from_path, try_from_path}` call `std::env::current_dir()` only for a
831 // RELATIVE path (EXECUTED: 1 `getcwd` for `rel/x`, 0 for an absolute path and for the empty path) and open nothing.
832 // `Print::write_ansi` and `SetTitle::write_ansi` write the caller's `T: Display` into the caller's writer: EXECUTED 0 events for
833 // `&str`, and 1 `NO_COLOR` read for `Print(Colored::..)`, so the effect is the type argument's. `SetTitle::execute_winapi` (a
834 // console-title call) and `ansi_support::supports_ansi` (console VT mode, then `TERM` only when that fails) are `cfg(windows)`:
835 // real, READ only, no host to execute them on, so no rule.
836 ("tempfile", "tempfile::file::TempPath::from_path", "R1595:conditional"),
837 ("tempfile", "tempfile::file::TempPath::try_from_path", "R1595:conditional"),
838 // `TempDir::keep` and its deprecated `into_path` (R1595:overcharge, the scan oracle fabricates): the self-scan reads `Fs` because the
839 // consumed `self` drops at the end of the body and `TempDir`'s `Drop` removes the directory — but `keep` has just set
840 // `disable_cleanup`, so nothing is removed (EXECUTED: 0 `unlink`, 0 `mkdir`, 0 `open`, 0 `getenv` for both). Carved out of the `::keep`
841 // suffix rule for the same reason; that rule had charged `TempDir::keep` `Fs` since the sweep that wrote it.
842 ("tempfile", "tempfile::dir::TempDir::keep", "R1595:overcharge"),
843 ("tempfile", "tempfile::dir::TempDir::into_path", "R1595:overcharge"),
844 ("crossterm", "crossterm::style::Print::write_ansi", "R1595:conditional"),
845 ("crossterm", "crossterm::terminal::SetTitle::write_ansi", "R1595:conditional"),
846 ("crossterm", "crossterm::terminal::SetTitle::execute_winapi", "R1595:silent"),
847 ("crossterm", "crossterm::ansi_support::supports_ansi", "R1595:silent"),
848 // The last nine rows of the R1528 backlog (b11, R1598), every one `conditional`, each with an EXECUTED arm on each side.
849 // deadpool-postgres 0.14.2 `Config::{get_pg_config, builder, create_pool}` read `USER` ONLY when neither the config's `user` nor
850 // the url's names one (EXECUTED 1 `getenv USER` with no user, 0 with `user` set, 0 with a url that carries one, 1 with a url that
851 // does not); they open no connection (the pool is lazy: 0 connections). A rule would charge every configured pool.
852 ("deadpool_postgres", "deadpool_postgres::config::Config::get_pg_config", "R1598:conditional"),
853 ("deadpool_postgres", "deadpool_postgres::config::Config::builder", "R1598:conditional"),
854 ("deadpool_postgres", "deadpool_postgres::config::Config::create_pool", "R1598:conditional"),
855 // grep-cli 0.1.12. `resolve_binary` and the default-command resolution inside `DecompressionMatcher::new` and
856 // `DecompressionMatcherBuilder::build` search `PATH` and `stat` each directory only under `cfg!(windows)` (EXECUTED on macOS: 0
857 // events for all three; `try_associate`/`associate`, which call the unconditional resolver, ARE charged `Env`). `build` was
858 // charged `Exec` by the `::build` suffix rule meant for `CommandReaderBuilder::build` and spawns nothing (EXECUTED 0
859 // fork/spawn/exec), so it is carved out of it and left here. `is_readable_stdin` is an `fstat` on stdin (EXECUTED: `isatty` and
860 // one `ioctl`, no read) plus `log::debug!` records that emit only under a debug-level logger.
861 ("grep_cli", "grep_cli::decompress::DecompressionMatcher::new", "R1598:conditional"),
862 ("grep_cli", "grep_cli::decompress::DecompressionMatcherBuilder::build", "R1598:conditional"),
863 ("grep_cli", "grep_cli::decompress::resolve_binary", "R1598:conditional"),
864 ("grep_cli", "grep_cli::is_readable_stdin", "R1598:conditional"),
865 // rand 0.10.3 `Fill::fill_slice` draws from the caller's `R` (EXECUTED: 0 entropy reads for a seeded `StdRng`, 1 for the thread
866 // RNG). tokio-postgres 0.7.18 `Connection::poll_message` drives the caller's stream `S` (EXECUTED: 65 bytes up / 92 down over TCP
867 // with a query in flight, 0 with nothing in flight, 0 over an in-memory `duplex` whose server never opens a socket).
868 ("rand", "rand::rng::u8::fill_slice", "R1598:conditional"),
869 ("tokio_postgres", "tokio_postgres::connection::Connection::poll_message", "R1598:conditional"),
870 // SOUNDNESS R1528 — the 2026-11-11 weekly refresh (64 grown rows on main aacd3880; 58 at 7e8870b, reproduced exactly on macOS, so
871 // the checked-in-macOS / CI-Linux split (R212) explains none). 30 got classify rules (postgres sync, sqlx Migrator, redis PubSub,
872 // mongodb Aggregate, async-nats unpin_consumer, sea-orm accepts, tracing-subscriber OffsetTime); the 34 below hold a verdict.
873 //
874 // R1693 (conditional x16): chrono 0.4.45 `Date<Tz>::{and_time, and_hms*}` and `DateTime<Tz>::{checked_add/sub_days,
875 // checked_add/sub_signed, with_time}` forward to `TimeZone::from_local_datetime`, so the effect is the caller's `Tz`: EXECUTED
876 // (macOS interposer, TZ unset, offsets built without reading the zone) `Local` = getenv(TZ) + lstat/open(/etc/localtime) + 1
877 // clock read for and_hms_opt, and_time, checked_add_days, checked_add_signed, with_time (and `Local::from_utc_datetime`), `Utc` =
878 // 0 events on the same five; the other seven spellings share those bodies and are READ. diesel 2.3.14 `DateTime::from_sql` is the
879 // same fact one hop out: the key merges the `DateTime<Utc>` impl (`Utc.from_utc_datetime`, pure) with the `DateTime<Local>` one
880 // (`Local::from_utc_datetime`, the executed read) — READ, not executed.
881 ("chrono", "chrono::date::Date::and_hms", "R1693:conditional"),
882 ("chrono", "chrono::date::Date::and_hms_micro", "R1693:conditional"),
883 ("chrono", "chrono::date::Date::and_hms_micro_opt", "R1693:conditional"),
884 ("chrono", "chrono::date::Date::and_hms_milli", "R1693:conditional"),
885 ("chrono", "chrono::date::Date::and_hms_milli_opt", "R1693:conditional"),
886 ("chrono", "chrono::date::Date::and_hms_nano", "R1693:conditional"),
887 ("chrono", "chrono::date::Date::and_hms_nano_opt", "R1693:conditional"),
888 ("chrono", "chrono::date::Date::and_hms_opt", "R1693:conditional"),
889 ("chrono", "chrono::date::Date::and_time", "R1693:conditional"),
890 ("chrono", "chrono::datetime::DateTime::checked_add_days", "R1693:conditional"),
891 ("chrono", "chrono::datetime::DateTime::checked_add_signed", "R1693:conditional"),
892 ("chrono", "chrono::datetime::DateTime::checked_sub_days", "R1693:conditional"),
893 ("chrono", "chrono::datetime::DateTime::checked_sub_signed", "R1693:conditional"),
894 ("chrono", "chrono::datetime::DateTime::with_time", "R1693:conditional"),
895 ("diesel", "diesel::pg::types::date_and_time::chrono::DateTime::from_sql", "R1693:conditional"),
896 ("diesel", "diesel::sqlite::types::date_and_time::chrono::DateTime::from_sql", "R1693:conditional"),
897 // R1694 (overcharge x4): the self-scan fabricates the effect on a body that reads none. (a) chrono `NaiveDateTime::
898 // {parse_from_str, parse_and_remainder}` and `Parsed::to_naive_datetime_with_offset` are arithmetic on `NaiveDate`/`NaiveDateTime`/
899 // `Parsed`, but `date.and_time(t)` / `datetime.hour()` on those receivers are resolved to the `Date<Tz>` / `DateTime<Tz>`
900 // generic impls and so inherit the `Local` arm of R1693 (EXECUTED: all three 0 events). (b) ureq 3.5.1 `Config::new_agent` is
901 // `self.clone().into()` -> `Agent::new_with_config`, already `REVIEWED_PURE_ENTRIES`; the scan charges the pool's connector
902 // CLOSURE (built, never called) at its creator (EXECUTED: 0 connect, 0 getenv in the window).
903 ("chrono", "chrono::format::parsed::Parsed::to_naive_datetime_with_offset", "R1694:overcharge"),
904 ("chrono", "chrono::naive::datetime::NaiveDateTime::parse_and_remainder", "R1694:overcharge"),
905 ("chrono", "chrono::naive::datetime::NaiveDateTime::parse_from_str", "R1694:overcharge"),
906 ("ureq", "ureq::config::Config::new_agent", "R1694:overcharge"),
907 // R1695 (conditional x11): ureq 3.5.1 `Body`/`BodyWithConfig` readers and `Body::as_body` take their effect from where the body
908 // came from — a connection (`BodyDataSource::Handler`: the read blocks on the socket, EXECUTED 810 ms against a server that
909 // withholds the tail 0.8 s; the Net is charged at `.call()`) or memory (`Body::builder().data(..)`, 0 ms, EXECUTED); the oracle's
910 // `Log` is a `debug!` on gzip/brotli/charset/unknown-encoding only. `DefaultResolver::host_and_port` is a `format!`; its `Log`
911 // is `Scheme::default_port`'s `debug!("Unknown scheme")`, emitted only for a non-http(s)/proxy scheme under a debug logger.
912 ("ureq", "ureq::body::Body::as_reader", "R1695:conditional"),
913 ("ureq", "ureq::body::Body::into_reader", "R1695:conditional"),
914 ("ureq", "ureq::body::Body::read_json", "R1695:conditional"),
915 ("ureq", "ureq::body::Body::read_to_string", "R1695:conditional"),
916 ("ureq", "ureq::body::Body::read_to_vec", "R1695:conditional"),
917 ("ureq", "ureq::body::BodyWithConfig::read_json", "R1695:conditional"),
918 ("ureq", "ureq::body::BodyWithConfig::read_to_string", "R1695:conditional"),
919 ("ureq", "ureq::body::BodyWithConfig::read_to_vec", "R1695:conditional"),
920 ("ureq", "ureq::body::BodyWithConfig::reader", "R1695:conditional"),
921 ("ureq", "ureq::send_body::Body::as_body", "R1695:conditional"),
922 ("ureq", "ureq::unversioned::resolver::DefaultResolver::host_and_port", "R1695:conditional"),
923 // R1553 (+2): `ConfigBuilder::timeout_per_read` / `timeout_per_write`, setters new in ureq 3.5.x and the same shape as the 30 above.
924 ("ureq", "ureq::config::ConfigBuilder::timeout_per_read", "R1553:conditional"),
925 ("ureq", "ureq::config::ConfigBuilder::timeout_per_write", "R1553:conditional"),
926 // R1696 (conditional x1): ureq 3.5.1 `TcpStream::shutdown` is `shutdown(2)` on the socket, but `mod tcp;` is private and the type
927 // has no re-export, so no consumer spelling reaches it; the one consumer route is `Agent::shutdown`, which the scan marks
928 // `Unknown` alone (a `dyn ShutdownHandle`) and which now carries a `Net` rule (EXECUTED: a worker blocked in a body read ended at
929 // 260 ms with an error after `agent.shutdown()`, ran 811 ms to success without it).
930 ("ureq", "ureq::unversioned::transport::tcp::TcpStream::shutdown", "R1696:conditional"),
931 // R1732 (overcharge x3): reqwest 0.13.5 blocking `Part::text`, `Part::bytes` and `Form::text` build an in-memory `Bytes` body
932 // (`Body::from(&'static str | String | &'static [u8] | Vec<u8>)`, then `Part::new`): no `std::fs` anywhere in the three bodies.
933 // The `Fs` is blocking `Body`'s SIX `From<_> for Body` impls sharing one key, one of which (`From<File>`, `f.metadata()`)
934 // reads the file; `Part::text`'s `Body::from(..)` call used to be disclosed `ambiguous:same-name local defs` and R1657
935 // stage 2's re-export table now resolves it to the merged unit (`CANDOR_R1657S2_OFF` turns the `Fs` off). `Part::file` /
936 // `Form::file`, which really open a path, keep their rule.
937 ("reqwest", "reqwest::blocking::multipart::Form::text", "R1732:overcharge"),
938 ("reqwest", "reqwest::blocking::multipart::Part::bytes", "R1732:overcharge"),
939 ("reqwest", "reqwest::blocking::multipart::Part::text", "R1732:overcharge"),
940 // R1733 (conditional x1): rustls 0.23.46 `Accepted::into_connection` runs the ClientHello handshake step, which calls
941 // `config.key_log.log(..)` (`dyn KeyLog`): with `ServerConfig::key_log = Arc::new(KeyLogFile::new())` and `SSLKEYLOGFILE` set it
942 // APPENDS to that file (EXECUTED: 5 lines written), with the default `NoKeyLog` nothing is touched (EXECUTED: no file). The
943 // report's `Fs` is the `dyn KeyLog` union (`KeyLogFile::log` is the one visible implementor that writes), surfaced when R1657
944 // stage 2 resolved `with_certified_key`'s `handle_client_hello` calls that were `ambiguous:same-name local methods`.
945 ("rustls", "rustls::server::server_conn::Accepted::into_connection", "R1733:conditional"),
946 // R1734 (overcharge x1): redis 1.8.1 `ClusterClient::new` is `Self::builder(nodes).build()`; `ClusterClientBuilder::build` is a
947 // `REVIEWED_PURE_ENTRIES` read (PEM bytes the caller supplied, nothing opened). The scan dropped the `.build()` edge on a
948 // `Self::builder(..)` receiver until R1717's builder typing (`CANDOR_R1717_OFF` makes the row vanish), so the wrapper now inherits the
949 // `Fs` the `pem` vocabulary puts on `ClusterParams::from` -> `retrieve_tls_certificates`. The edge is real; the effect is not.
950 ("redis", "redis::cluster_handling::client::ClusterClient::new", "R1734:overcharge"),
951];
952
953/// Representative path tails (each appended to a crate name) that the `calibrated_crates_are_live`
954/// liveness test probes: at least one must match for every `CALIBRATED_CRATES` entry, else the entry is
955/// dead. Exported as ONE source of truth because the nightly lint crate (`src/lib.rs`) runs the SAME
956/// liveness test — when the two probe lists were duplicated they drifted, and a rule keyed on a
957/// distinctive tail (pnet `::datalink::channel`, ignore `::WalkBuilder::build_parallel`, notify
958/// `::RecommendedWatcher::new`) added to only one list silently broke the other crate's `cargo test`.
959pub const CALIBRATION_PROBE_TAILS: &[&str] = &[
960 "::X::send", "::X::execute", "::X::call", "::X::query", "::X::fetch_one", "::Remote::fetch",
961 "::datalink::channel", "::WalkBuilder::build_parallel", "::RecommendedWatcher::new",
962 "::X::connect", "::Utc::now", "::X::load", "::__private_api::log", "::tempfile", "::glob",
963 "::X::run", "::dotenv", "::random", "::emit", "::X::emit_span_lint", "::X::anything",
964 "::X::draw",
965 "::SaltString::generate", "::hash", "::OsRng::fill_bytes",
966 // verb-precise crates whose whole-crate rules were narrowed to the effectful surface (the pure
967 // accessors/ctors/data-types now return None), so the liveness probe must name an EFFECTFUL path:
968 "::Mmap::map", "::event", "::u32", "::Clipboard::get_text", "::spawn_command",
969 // coverage-differential crates (each needs ≥1 effectful tail; existing tails already cover
970 // native_tls_crate/tokio_native_tls/sqlx_core via ::X::connect, execute via ::X::execute, jiff via ::now):
971 "::read_tls", "::home_dir", "::args", "::from_env", "::IntoIter::next", "::set_file_mtime",
972 "::surely_conflicts_with", "::set_handler", "::get_matches", "::init", "::interact",
973 "::write_line", "::background_color", "::retry", "::build",
974];
975
976/// Database client crates whose execution verbs are I/O (see the DB branch in `classify`).
977/// Module-level so `db_crates_are_calibrated` can enforce `DB_CRATES ⊆ CALIBRATED_CRATES`.
978pub const DB_CRATES: [&str; 11] = [
979 "sqlx", "rusqlite", "postgres", "tokio_postgres", "diesel", "redis", "mongodb",
980 "mysql", "mysql_async", "sea_orm", "deadpool_postgres",
981];
982
983/// Pure file-descriptor *ownership-transfer* leaves. These ADOPT an already-open descriptor
984/// (`from_raw_fd`/`from_raw_socket`/`from_raw_handle`), EXTRACT/BORROW one
985/// (`into_raw_fd`/`into_raw_socket`/`into_raw_handle`, `as_raw_fd`/`as_raw_socket`/`as_raw_handle`),
986/// or UNWRAP an async wrapper back to its std type (`into_std`) — none of them issue a syscall or
987/// perform I/O. calling a PURE function effectful is a FABRICATION — the precision failure (candor's cardinal sin is the opposite direction, the silent under-report) — and these collide with the
988/// coarse std-type PREFIX rules (`std::net::TcpStream`/`std::fs::File`/`std::os::unix::net` → Net/Fs/Ipc)
989/// even though the descriptor was opened ELSEWHERE. The portable_pty/async_process Exec rule already
990/// exempts `from_raw_fd`; this generalises the same carve-out across the net/fs/ipc prefix rules.
991/// (Found by a real-world sweep of tokio: `TcpStream::into_std`, `*::from_raw_fd`, `*::as_raw_fd` all
992/// fabricated Net/Fs/Ipc.)
993const PURE_FD_TRANSFER: &[&str] = &[
994 "from_raw_fd", "from_raw_socket", "from_raw_handle",
995 "into_raw_fd", "into_raw_socket", "into_raw_handle",
996 "as_raw_fd", "as_raw_socket", "as_raw_handle",
997 "into_std",
998 // `SocketAddr::from_pathname` (std/async-std unix net) builds an address STRUCT from a path —
999 // it opens no socket. The `std::os::unix::net` prefix rule below would otherwise fabricate Ipc
1000 // on it. (Found sweeping socket2: `SockAddr::as_unix` → `from_pathname` reported Ipc.)
1001 "from_pathname",
1002];
1003
1004/// The std I/O **handle** types whose methods the scanner's receiver inference may route into
1005/// `classify` as `Type::method`.
1006///
1007/// WHY A LIST AND NOT "std". The std rules in `classify` are a MIXTURE: a few are type-precise with
1008/// reviewed pure-accessor carve-outs (`std::net::TcpStream` → Net minus `local_addr`/`peer_addr`/…;
1009/// `std::process::Command` → Exec minus `get_program`/`get_args`/…), but most are coarse MODULE
1010/// prefixes (`std::fs::` → Fs) written for free functions and constructors. Applied to arbitrary
1011/// method calls, a coarse prefix charges an effect for reading a struct field: `std::fs::Metadata`,
1012/// `DirEntry`, `Permissions` and `FileType` all sit under `std::fs::` and are pure DATA. So the
1013/// scanner used to skip EVERY std-rooted receiver — which shut the door on the pure data types and on
1014/// the real handles alike, and `fn run(cmd: &mut Command) { cmd.spawn(); }` certified PURE while it
1015/// spawned a process (a silent false all-clear; the `cc` crate's `command_helpers::spawn` was a live
1016/// instance). This list re-opens the door for the handles ONLY.
1017///
1018/// MEMBERSHIP RULE, and the reason it is safe to apply a whole-type rule to an inferred method call:
1019/// a type belongs here only when EVERY method on it is either the effect itself or an already-carved-
1020/// out pure read-back. That holds for an open descriptor's handle — a `File`/`TcpStream`/`Child` does
1021/// nothing but I/O on the thing it owns — and the `PURE_FD_TRANSFER` guard at the top of `classify`
1022/// exempts the `as_raw_fd`/`into_std` family for all of them before any prefix rule runs.
1023///
1024/// THE OPTION-BUILDERS, `OpenOptions` and `DirBuilder`, were absent for exactly this rule and now
1025/// satisfy it: each has a type-keyed carve-out in `classify` subtracting its pure setters, so only its
1026/// TERMINAL VERB (`open` / `create`) is Fs. The exclusion was written as "a narrow residual, not the
1027/// reported hole" and MEASURED otherwise — `fn load(o: &OpenOptions, p: &Path) { o.open(p) }` opened a
1028/// file and reported NOTHING, the same silent false all-clear as the `Command` parameter, while a
1029/// setter-only `OpenOptions::new().read(true)` reported `Fs` for opening nothing. The order the old
1030/// comment prescribed is the order taken: the verb-precise rules went in FIRST, and this list widened
1031/// onto them. `ReadDir` stays out — its `next` IS a syscall, but nothing distinguishes it from any
1032/// other iterator's `next`, so no verb-precise rule can be written and the honest miss stands.
1033///
1034/// `std::path::Path`/`PathBuf` are NOT here: they are pure data with an effectful STAT sub-surface —
1035/// the exact inverse shape — and they route through their own verb-precise carve-out in the scanner.
1036const STD_EFFECT_HANDLES: &[&str] = &[
1037 // Exec — the whole type is the subprocess boundary (`new` names a program, `arg`/`env` build the
1038 // invocation, `spawn`/`output`/`status` run it); the read-back getters are already carved out.
1039 "std::process::Command",
1040 "std::process::Child",
1041 // Net — an open socket. `connect`/`accept`/`read`/`write`/`shutdown`/`set_*` are socket syscalls;
1042 // `local_addr`/`peer_addr`/`nodelay`/`ttl`/`take_error` are carved out.
1043 "std::net::TcpStream",
1044 "std::net::TcpListener",
1045 "std::net::UdpSocket",
1046 // Ipc — a Unix-domain socket, the same shape as the TCP/UDP handles above.
1047 "std::os::unix::net::UnixStream",
1048 "std::os::unix::net::UnixListener",
1049 "std::os::unix::net::UnixDatagram",
1050 // Fs — an open file. Every method reads, writes, syncs, truncates or stats it; the descriptor
1051 // conversions are carved out by `PURE_FD_TRANSFER`.
1052 "std::fs::File",
1053 // Fs — the option-builders, whose PURE setters are subtracted by the type-keyed carve-outs in
1054 // `classify` so that only the terminal verb (`OpenOptions::open`, `DirBuilder::create`) routes to Fs.
1055 "std::fs::OpenOptions",
1056 "std::fs::DirBuilder",
1057];
1058
1059/// Is `ty` a std I/O handle type whose methods may be routed into `classify` by the scanner's
1060/// receiver inference? See `STD_EFFECT_HANDLES` for the membership rule and the named exclusions.
1061pub fn is_std_effect_handle(ty: &str) -> bool {
1062 STD_EFFECT_HANDLES.contains(&ty)
1063}
1064
1065/// Classify a resolved callee by the crate it belongs to and its full path.
1066pub fn classify(crate_name: &str, path: &str) -> Option<&'static str> {
1067 // Pure fd ownership-transfer/extraction leaves are never an effect, regardless of which std I/O
1068 // type they hang off — exempt them BEFORE the coarse prefix rules can fabricate Net/Fs/Ipc.
1069 if PURE_FD_TRANSFER.contains(&path.rsplit("::").next().unwrap_or(path)) {
1070 return None;
1071 }
1072 // SOUNDNESS R1528 (b11) — the last grandfathered rows. See `b11_r1528`; `Some(None)` is a proven-pure CARVE-OUT of a
1073 // suffix rule further down (`DecompressionMatcherBuilder::build` is not the `::build` that spawns).
1074 if let Some(verdict) = b11_r1528(crate_name, path) {
1075 return verdict;
1076 }
1077 if crate_name.starts_with("aws_sdk_") || crate_name.starts_with("aws_smithy") {
1078 // Only request dispatch is network I/O; builder setters/accessors are pure.
1079 if path.ends_with("::send") || path.ends_with("::send_with") {
1080 return Some("Net");
1081 }
1082 return None;
1083 }
1084 // aws-config resolves credentials/region on `.load()` — it reaches the IMDS metadata
1085 // endpoint / STS over the network (and reads ~/.aws + env). Builders (`defaults()`,
1086 // `SdkConfig::builder()`, `BehaviorVersion::latest()`) are pure; the `load` is the I/O.
1087 // (Found hardening on a real app, ebman: `builder.load().await` was classified pure.)
1088 //
1089 // THE FIX: `load_from_env()` (lib.rs:170/188, both the `behavior-version-latest` and deprecated
1090 // forms) is `from_env().load().await` / `load_defaults(latest).await` one call down — the crate's OWN
1091 // "convenience wrapper" (its own doc comment's phrase) around the already-modelled `.load()` — but
1092 // `"load_from_env".ends_with("::load")` is false, so the wrapper read pure. Same vein as
1093 // `ignore::Walk::new`.
1094 if crate_name == "aws_config" {
1095 if path.ends_with("::load") || path.ends_with("::load_defaults") || path.ends_with("::load_from_env") {
1096 return Some("Net");
1097 }
1098 // SOUNDNESS R1528 — aws-config 1.12.0, the grandfathered rows that are not `.load()`. See `aws_config_r1528`.
1099 return aws_config_r1528(path);
1100 }
1101 // git2 (libgit2 FFI): remote operations contact the network; everything else is local
1102 // to the .git directory. Match the remote verbs precisely — NOT bare `::clone`, which is
1103 // the `Clone`-trait dup of a `Remote` handle (pure), not `Repository::clone`. (Found
1104 // hardening on gitui: `remote.fetch`/`remote.push` were classified network-free — a git
1105 // client reporting it makes no network calls.)
1106 //
1107 // `Repository::clone`/`clone_recurse`/`clone_local`/`init` (via `RepoBuilder::clone`) IS the
1108 // thing the comment above named and then excluded anyway: the bare `::clone` denylist meant
1109 // to keep out `Remote::clone` (the derived trait dup) also swallowed `Repository::clone` —
1110 // libgit2's actual network clone, and arguably git2's single most common entry point. A
1111 // fabrication corpus round caught it: `git2::Repository::clone(url, path)` reported ZERO
1112 // effects and passed `deny Net` at exit 0. FQN-exact (like the `reqwest::get` disambiguation
1113 // a few lines up) so the fix doesn't just re-widen `::clone` and reintroduce the very
1114 // over-charge the comment was written to prevent: `Remote::clone` (and `Repository::clone`'s
1115 // own `Clone` derive, if it has one) must still read pure.
1116 if crate_name == "git2" {
1117 if path.ends_with("::fetch")
1118 || path.ends_with("::push")
1119 || path.ends_with("::download")
1120 || path.ends_with("::connect")
1121 || path.ends_with("::connect_auth")
1122 || path.ends_with("::ls")
1123 || path.ends_with("::upload")
1124 || path == "git2::Repository::clone"
1125 || path == "git2::Repository::clone_recurse"
1126 || path == "git2::build::RepoBuilder::clone"
1127 // A FOURTH+FIFTH true positive in the same gap the `Repository::clone` fix left open:
1128 // `Submodule::clone`/`Submodule::update` (submodule.rs:39,234) call `raw::git_submodule_clone`/
1129 // `git_submodule_update` DIRECTLY — the exact FFI leaves already in this file's own
1130 // FFI-tier NET table below, but only when a caller names the raw `ffi`/`raw::` leaf itself.
1131 // git2's documented, standard submodule-init idiom (`sub.clone(None)` /
1132 // `sub.update(true, None)`) never does that — it calls the safe wrapper, which carried no
1133 // rule of its own and read pure under `deny Net`. FQN-exact, matching the `Repository::clone`
1134 // fix's own discipline: a bare `::update`/`::clone` substring would sweep in git2's many pure
1135 // `update_*` setters (`CheckoutBuilder::update_only`, `DiffOptions::update_index`, …) and the
1136 // derive-`Clone` dup on every other git2 type.
1137 || path == "git2::Submodule::clone"
1138 || path == "git2::Submodule::update"
1139 // `Remote::list`/`RemoteConnection::list` (remote.rs:378,755) call `raw::git_remote_ls`
1140 // directly — the reference-advertisement fetch already in the FFI-tier NET table below,
1141 // reached the same way `Submodule::clone`/`update` were: the safe wrapper's OWN name
1142 // (`list`, not `ls`) never matched the `::ls` suffix this block already carried (which, on
1143 // git2 0.20, matches no real method at all — dead weight kept for whatever older API it once
1144 // fit). FQN-exact so a `list` on some other git2 type (none exist today, but the same
1145 // discipline as `Submodule::clone` above) can't be swept in by a bare suffix later.
1146 || path == "git2::Remote::list"
1147 || path == "git2::RemoteConnection::list"
1148 {
1149 return Some("Net");
1150 }
1151 // THE COVERAGE-GATE SWEEP (2026-08-27): every one of these is a public entry point whose body
1152 // calls a `raw::git_*` FFI leaf ALREADY in this file's own FS FFI-tier table below (config.rs/
1153 // index.rs/odb.rs/packbuilder.rs/reference.rs/repo.rs/treebuilder.rs, verified against git2
1154 // 0.20.4 source) — but that table is UNREACHABLE for a real consumer's call, because it only
1155 // fires when self-scan resolves the FFI leaf's OWN crate (`libgit2_sys`, via git2's internal
1156 // `use libgit2_sys as raw`), never for `crate_name == "git2"`, which returns from THIS block
1157 // first. A consumer never spells `raw::git_repository_open`; they call `git2::Repository::open`,
1158 // which fell through this entire branch to `None` — the exact `ignore::Walk::new` shape (a
1159 // lower-level rule the top-level entry point never reaches) but affecting git2's single most
1160 // common local operations (`Repository::open`/`init`), not an edge case. FQN-exact, not a
1161 // `::open`/`::write`/`::read` suffix — those verbs are common enough elsewhere (`Odb::reader`
1162 // isn't `::read`, `TreeBuilder`/`Index`/`PackBuilder::write` share the name with plenty of pure
1163 // builder setters on other types) that a bare suffix would either miss these or over-charge.
1164 if path == "git2::Config::add_file"
1165 || path == "git2::Config::open"
1166 || path == "git2::Config::open_default"
1167 || path == "git2::Index::add_all"
1168 || path == "git2::Index::add_path"
1169 || path == "git2::Index::read"
1170 || path == "git2::Index::write"
1171 || path == "git2::Index::write_tree"
1172 || path == "git2::Index::write_tree_to"
1173 || path == "git2::Odb::read"
1174 || path == "git2::Odb::reader"
1175 || path == "git2::Odb::write"
1176 || path == "git2::Odb::writer"
1177 || path == "git2::PackBuilder::write"
1178 || path == "git2::Reference::delete"
1179 || path == "git2::Reference::set_target"
1180 || path == "git2::Repository::blob_path"
1181 || path == "git2::Repository::checkout_head"
1182 || path == "git2::Repository::checkout_index"
1183 || path == "git2::Repository::checkout_tree"
1184 || path == "git2::Repository::commit"
1185 || path == "git2::Repository::discover"
1186 || path == "git2::Repository::discover_path"
1187 || path == "git2::Repository::init"
1188 || path == "git2::Repository::init_bare"
1189 || path == "git2::Repository::init_opts"
1190 || path == "git2::Repository::open"
1191 || path == "git2::Repository::open_bare"
1192 || path == "git2::Repository::open_ext"
1193 || path == "git2::Repository::open_from_env"
1194 || path == "git2::Repository::reference"
1195 || path == "git2::Repository::tag"
1196 || path == "git2::TreeBuilder::write"
1197 {
1198 return Some("Fs");
1199 }
1200 // `Cred::credential_helper` (cred.rs:121) calls `CredentialHelper::execute` (cred.rs:326),
1201 // which — on every code path — spawns a REAL `sh -c "<helper> get"` (or, if `sh` itself fails to
1202 // spawn, the helper binary directly) via `std::process::Command::spawn` to resolve
1203 // `credential.helper`-configured auth (cred.rs:370-390): a real subprocess launch a caller
1204 // authenticating against a private remote is one call away from (`Cred::credential_helper(cfg,
1205 // url, user)` is the crate's own documented way to honor `credential.helper`), not a lower-level
1206 // implementation detail. FQN-exact: `execute` alone is far too generic to key on blindly.
1207 if path == "git2::Cred::credential_helper" || path == "git2::CredentialHelper::execute" {
1208 return Some("Exec");
1209 }
1210 return None;
1211 }
1212 // libc — raw syscalls via FFI. The FFI-thin tier (nix, and the syscall layer beneath rusqlite/git2)
1213 // is invisible to a name classifier unless we model libc directly: a 35-crate calibration
1214 // (eval/calibration) showed nix reporting ZERO library effects because every wrapper bottoms out in
1215 // an unrecognised `libc::*` call. Classify by syscall name, but ONLY the UNAMBIGUOUS ones — the
1216 // socket family is Net, path/dir syscalls are Fs, spawn/exec/wait is Exec, SysV/pipe IPC is Ipc,
1217 // env/clock/entropy each their own. We deliberately SKIP the generic file-descriptor ops
1218 // (read/write/close/lseek/dup/fcntl/ioctl/poll/select/epoll*/mmap): they operate on ANY fd — file,
1219 // socket, or pipe — so a fixed label would mis-categorise as often as it helps. An honest
1220 // no-classify (under-report) beats emitting the WRONG effect. Pure conversions (htons/inet_pton/
1221 // gmtime) are also skipped.
1222 //
1223 // `nix` (the idiomatic SAFE libc wrapper, in ~every Rust systems/CLI crate) is routed through the
1224 // SAME table: its functions keep the syscall leaf name (`nix::fcntl::open`, `nix::sys::socket::connect`,
1225 // `nix::unistd::execvp`). Without this, a CONSUMER of nix analysed without nix's own source (the
1226 // stable scanner, single-crate) sees `nix::*` cross-crate and under-reports — serialport-rs opens its
1227 // device via `nix::fcntl::open` and reported ZERO Fs. The nightly lint reaches `libc::*` THROUGH nix's
1228 // body; this gives the scanner the same coverage directly. (Found sweeping serialport-rs.)
1229 // `rustix` is the same shape as nix but does RAW syscalls (no libc underneath), so its functions MUST
1230 // be classified directly. Its leaf names are the syscall names too (`rustix::time::clock_settime`,
1231 // `rustix::fs::mkfifoat`/`symlink`/`stat`, `rustix::net::connect`) — route it through the same table.
1232 // The rustix-specific `*at`/variant leaves it doesn't share with libc just under-report (the safe
1233 // direction). VALIDATED, not speculative: coreutils' `date` reads/sets the clock via
1234 // `rustix::time::clock_getres`/`clock_settime` and reported Clock=0; the file I/O that goes through
1235 // std::fs was already correct, which is why only the rustix-only effects (Clock/Ipc) were missing.
1236 if crate_name == "libc" || crate_name == "nix" || crate_name == "rustix" {
1237 let f = path.rsplit("::").next().unwrap_or(path);
1238 // path / directory / metadata syscalls (incl. *64 and *at variants) — `LIBC_FS`, hoisted to module
1239 // level so the AS-EFF-008 masking guard reads THE SAME TABLE (SOUNDNESS R806); see `masks_locator`.
1240 const FS: &[&str] = LIBC_FS;
1241 // socket family — these operate only on sockets, so Net is unambiguous (AF_UNIX domain isn't
1242 // visible at the call, so a Unix socket reads as Net rather than Ipc; acceptable over-general).
1243 const NET: &[&str] = LIBC_NET;
1244 // process creation / replacement / reaping
1245 const EXEC: &[&str] = LIBC_EXEC;
1246 // pipes / FIFOs / SysV + POSIX message queues, semaphores, shared memory; socketpair (AF_UNIX)
1247 const IPC: &[&str] = &[
1248 "pipe", "pipe2", "mkfifo", "mkfifoat", "socketpair", "msgget", "msgsnd", "msgrcv", "msgctl",
1249 "semget", "semop", "semtimedop", "semctl", "shmget", "shmat", "shmdt", "shmctl", "mq_open",
1250 "mq_send", "mq_receive", "mq_timedsend", "mq_timedreceive", "mq_close", "mq_unlink",
1251 ];
1252 const ENV: &[&str] = &["getenv", "secure_getenv", "setenv", "putenv", "unsetenv", "clearenv"];
1253 const CLOCK: &[&str] = &[
1254 "time", "gettimeofday", "clock_gettime", "clock_getres", "nanosleep", "clock_nanosleep",
1255 // SETTING the system clock is a clock effect too (was unclassified — found on coreutils `date`,
1256 // which sets it via `clock_settime`).
1257 "clock_settime", "settimeofday", "stime", "adjtime", "adjtimex", "clock_adjtime",
1258 ];
1259 const RAND: &[&str] = &["getrandom", "getentropy", "arc4random", "arc4random_buf", "arc4random_uniform"];
1260 if FS.contains(&f) {
1261 return Some("Fs");
1262 }
1263 if NET.contains(&f) {
1264 return Some("Net");
1265 }
1266 if EXEC.contains(&f) {
1267 return Some("Exec");
1268 }
1269 if IPC.contains(&f) {
1270 return Some("Ipc");
1271 }
1272 if ENV.contains(&f) {
1273 return Some("Env");
1274 }
1275 if CLOCK.contains(&f) {
1276 return Some("Clock");
1277 }
1278 if RAND.contains(&f) {
1279 return Some("Rand");
1280 }
1281 return None;
1282 }
1283 // C-library FFI bindings: libsqlite3 (under rusqlite) and libgit2 (under git2). Like the libc tier,
1284 // these crates are thin Rust over a C library, so their real I/O is invisible until the C entry
1285 // points are named. Match by the DISTINCTIVE C function name (`sqlite3_*` / `git_*`) via the call's
1286 // LEAF — independent of the binding crate's alias: rusqlite calls `ffi::sqlite3_step`, git2 calls
1287 // `raw::git_remote_fetch`, and the nightly lint resolves the same to `libsqlite3_sys`/`libgit2_sys`;
1288 // all spellings share the leaf. Only the I/O-performing entry points are listed — the in-memory
1289 // accessors (`sqlite3_bind_*`/`sqlite3_column_*`, `git_*_oid`/strarray/options builders) stay pure,
1290 // so a non-listed `sqlite3_`/`git_` leaf returns None (under-report, never a wrong effect). Calibrated
1291 // + validated against rusqlite 0.39 / git2 0.20 source (eval/calibration).
1292 {
1293 let leaf = path.rsplit("::").next().unwrap_or(path);
1294 if let Some(rest) = leaf.strip_prefix("sqlite3_") {
1295 let _ = rest;
1296 // SQLite C API operations that touch the database (open/exec/step/prepare/backup/blob/wal).
1297 //
1298 // SOUNDNESS R166 — MEMBERSHIP CRITERION, stated because the list was extended and the next
1299 // reader needs to know what "already audited" covers. A leaf belongs here when it either
1300 // (a) reads or writes DATABASE CONTENT (disk or the in-memory image), or (b) mutates the
1301 // registry of code the engine will later RUN — the extension/auto-extension registry, whose
1302 // effect is not local to the calling frame. The audit boundary was the WHOLE `sqlite3_*`
1303 // surface rusqlite 0.40.2 mentions (185 distinct symbols diffed against this list), not the
1304 // one name the row was filed for: `sqlite3_auto_extension` arrived with its `_cancel_`/
1305 // `_reset_` siblings and with four content-I/O leaves nobody had looked at.
1306 //
1307 // DELIBERATELY STILL ABSENT, so this is not read as "the surface is covered": the
1308 // CALLBACK-INSTALLING entry points (`sqlite3_create_function[_v2]`,
1309 // `sqlite3_create_window_function`, `sqlite3_create_collation_v2`, `sqlite3_create_module_v2`,
1310 // `sqlite3_*_hook`, `sqlite3_set_authorizer`, `sqlite3_progress_handler`, `sqlite3_trace_v2`).
1311 // Those register a UDF/hook on ONE connection for the life of that connection; they perform no
1312 // database I/O themselves, and the opaque-callback boundary is already disclosed by the
1313 // scanner's own `Unknown` at the call site that hands the function pointer across. The
1314 // auto-extension registry is the case that differs: it is PROCESS-GLOBAL and outlives every
1315 // connection, so its consequence cannot be attributed to a caller's own frame at all.
1316 // Unexamined and named as such: the libgit2 (`git_*`) table below got no equivalent sweep.
1317 const DB: &[&str] = &[
1318 "sqlite3_open", "sqlite3_open_v2", "sqlite3_open16", "sqlite3_close", "sqlite3_close_v2",
1319 "sqlite3_exec", "sqlite3_step", "sqlite3_prepare", "sqlite3_prepare_v2",
1320 "sqlite3_prepare_v3", "sqlite3_prepare16", "sqlite3_prepare16_v2", "sqlite3_prepare16_v3",
1321 "sqlite3_get_table", "sqlite3_backup_init", "sqlite3_backup_step", "sqlite3_backup_finish",
1322 "sqlite3_blob_open", "sqlite3_blob_read", "sqlite3_blob_write", "sqlite3_blob_reopen",
1323 "sqlite3_load_extension", "sqlite3_wal_checkpoint", "sqlite3_wal_checkpoint_v2",
1324 // (b) the PROCESS-GLOBAL extension registry — every one of these changes what a LATER
1325 // `sqlite3_open` on any connection will execute, and `sqlite3_load_extension` (already
1326 // above) is the same capability one call along.
1327 "sqlite3_auto_extension", "sqlite3_cancel_auto_extension", "sqlite3_reset_auto_extension",
1328 "sqlite3_enable_load_extension",
1329 // (a) further DATABASE-CONTENT I/O the original list missed: whole-image load/save,
1330 // a forced page-cache flush to disk, a direct VFS file operation, and arming the
1331 // automatic WAL checkpoint writes whose manual twin is already listed.
1332 "sqlite3_deserialize", "sqlite3_serialize", "sqlite3_db_cacheflush",
1333 "sqlite3_file_control", "sqlite3_wal_autocheckpoint",
1334 ];
1335 return DB.contains(&leaf).then_some("Db");
1336 }
1337 if leaf.starts_with("git_") {
1338 // libgit2: remote/transport operations contact the network … (incl. submodule clone/update,
1339 // which `git_clone`/fetch the subrepo over its remote — `allow_fetch` defaults on; an A/B on
1340 // git2 0.20 caught `Submodule::update`/`clone` reporting no `Net`).
1341 const NET: &[&str] = &[
1342 "git_clone", "git_remote_connect", "git_remote_connect_ext", "git_remote_fetch",
1343 "git_remote_download", "git_remote_upload", "git_remote_push", "git_remote_ls",
1344 "git_submodule_clone", "git_submodule_update",
1345 ];
1346 // … and repository/index/odb/checkout/ref/config operations touch the on-disk .git store.
1347 const FS: &[&str] = &[
1348 "git_repository_open", "git_repository_open_ext", "git_repository_open_bare",
1349 "git_repository_init", "git_repository_init_ext", "git_repository_discover",
1350 "git_checkout_tree", "git_checkout_head", "git_checkout_index", "git_index_read",
1351 "git_index_write", "git_index_write_tree", "git_index_write_tree_to",
1352 "git_index_add_bypath", "git_index_add_all", "git_odb_open", "git_odb_read",
1353 "git_odb_write", "git_odb_open_wstream", "git_odb_open_rstream",
1354 "git_blob_create_fromdisk", "git_blob_create_fromworkdir", "git_blob_create_from_disk",
1355 "git_blob_create_from_workdir", "git_blob_create_from_stream", "git_commit_create",
1356 "git_commit_create_v", "git_reference_create", "git_reference_set_target",
1357 "git_reference_delete", "git_config_open_default", "git_config_open_ondisk",
1358 "git_config_add_file_ondisk", "git_tag_create", "git_treebuilder_write",
1359 "git_packbuilder_write",
1360 ];
1361 if NET.contains(&leaf) {
1362 return Some("Net");
1363 }
1364 if FS.contains(&leaf) {
1365 return Some("Fs");
1366 }
1367 return None;
1368 }
1369 if leaf.starts_with("curl_") {
1370 // libcurl (under the `curl` crate, called `curl_sys::curl_*`). Only the entry points that
1371 // PERFORM network I/O: the blocking transfer (`curl_easy_perform`), raw socket send/recv,
1372 // the HTTP/2 keepalive PING (`upkeep`), and the multi-interface transfer pumps. The large
1373 // pure surface (setopt/init/cleanup/reset/getinfo/escape/multi_add_handle/fdset/info_read)
1374 // stays unclassified, as do `curl_multi_wait`/`poll` (readiness WAIT on sockets, no payload —
1375 // the loop's `perform` is the tagged boundary, per the I/O-boundary principle). An A/B on
1376 // curl 0.4 caught the whole crate reporting ZERO Net (`Easy::perform` read as pure).
1377 const NET: &[&str] = &[
1378 "curl_easy_perform", "curl_easy_send", "curl_easy_recv", "curl_easy_upkeep",
1379 "curl_multi_perform", "curl_multi_socket_action",
1380 ];
1381 return NET.contains(&leaf).then_some("Net");
1382 }
1383 if let Some(op) = leaf.strip_prefix("SSL_") {
1384 // OpenSSL (libssl, under the `openssl`/`native-tls` crates, called `ffi::SSL_*`). The TLS
1385 // handshake and record I/O run over the peer socket -> Net. Unlike libc read/write, an SSL_*
1386 // op is ~always over a network BIO (the rare memory-BIO/sans-IO case is the honest exception
1387 // we accept). The crypto surface (EVP_*/SHA*/AES*) and pure setup (SSL_CTX_new/SSL_set_fd) are
1388 // NOT here; `BIO_*` is skipped (a BIO may be memory or socket). Validated vs openssl 0.9 source.
1389 const SSL_NET: &[&str] = &[
1390 "connect", "accept", "do_handshake", "read", "read_ex", "write", "write_ex", "peek",
1391 "peek_ex", "shutdown",
1392 ];
1393 return SSL_NET.contains(&op).then_some("Net");
1394 }
1395 }
1396 // HTTP clients use the same builder pattern as the AWS SDK: only the dispatch is
1397 // I/O. (Found by the eval: ebman's reqwest calls to the Anthropic API + webhooks
1398 // were silently classified network-free because reqwest wasn't recognized.)
1399 if crate_name == "reqwest" || crate_name == "isahc" {
1400 // SOUNDNESS R1528 — isahc 2.0.1, the grandfathered rows that are not the `::send`/`::get` family. See `isahc_r1528`.
1401 if crate_name == "isahc" {
1402 if let Some(effect) = isahc_r1528(path) {
1403 return Some(effect);
1404 }
1405 }
1406 // The dispatch (`::send`/`::execute`) is the I/O. PLUS the one-shot CONVENIENCE functions
1407 // `reqwest::get` / `reqwest::blocking::get` / `isahc::get`, which send immediately — they're
1408 // an EXACT match (not `Client::get`, the builder) to avoid false-positiving the builder path.
1409 // (Found running on `xh`: a one-shot `reqwest::get(url)` was classified network-free.)
1410 if path.ends_with("::send")
1411 || path.ends_with("::execute")
1412 || path == "reqwest::get"
1413 || path == "reqwest::blocking::get"
1414 || path == "isahc::get"
1415 {
1416 return Some("Net");
1417 }
1418 // THE URL-BEARING BUILDER METHODS: `Client::{get,post,put,delete,patch,head,request}(URL)`.
1419 // Real code almost never uses `reqwest::get(url)`; the DOMINANT idiom is the builder chain
1420 // `Client::new().post(url).send()` / `Client::builder().build()?.post(url).send()`. The `.send()`
1421 // already classifies `Net` — but the URL literal rides the `.post(url)` call, NOT `.send()`, so
1422 // without classifying the URL-naming step `Net` the endpoint is NEVER captured and the `Llm`
1423 // host refinement can't fire (ebman's `api.anthropic.com` call read as bare Net, undisclosed as
1424 // Llm — the dogfood silent under-report). Classifying these `Net` (idempotent with the eventual
1425 // `.send()`) makes the scanner capture the URL from their string arg. `request(method, url)`'s
1426 // url is its SECOND arg — the scanner's first-string-literal capture still gets it when the
1427 // method is a literal string, and misses it (honest under-report) when the method is an
1428 // expression. The pure builder surface (`::header`, `::json`, `::body`, `::query`, …) stays None.
1429 // SOUNDNESS R1604 — the URL-naming suffix rule below also fires on `get` hops that read an in-memory header map.
1430 // See `http_header_get_r1604`.
1431 if http_header_get_r1604(path) {
1432 return None;
1433 }
1434 if path.ends_with("::get")
1435 || path.ends_with("::post")
1436 || path.ends_with("::put")
1437 || path.ends_with("::delete")
1438 || path.ends_with("::patch")
1439 || path.ends_with("::head")
1440 || path.ends_with("::request")
1441 {
1442 return Some("Net");
1443 }
1444 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against isahc 2.0.1 — isahc-only, since
1445 // reqwest has no equivalent free-function async family:
1446 //
1447 // `get_async`/`head_async`/`post_async`/`put_async`/`delete_async`/`send_async` (lib.rs:357-500)
1448 // are the crate's async one-shot convenience functions — `HttpClient::shared().get_async(uri)`
1449 // etc., the exact async mirror of the already-covered `isahc::get`, dispatching immediately.
1450 // `HttpClient::new`/`HttpClientBuilder::build` (client.rs:629,444) are genuinely NOT lazy like
1451 // reqwest's `Client::new`: `build()` unconditionally calls `agent_builder.spawn()`
1452 // (agent/mod.rs:63), which spawns a background OS thread running `AgentContext::run` — curl's
1453 // multi-handle event loop that DOES perform the real socket reads/writes for every request later
1454 // sent through this client. Confirmed via self-scan on isahc's own source: `AgentBuilder::spawn`
1455 // reaches `Net` through that same call chain, not a guess.
1456 if crate_name == "isahc"
1457 && (path == "isahc::get_async"
1458 || path == "isahc::head_async"
1459 || path == "isahc::post_async"
1460 || path == "isahc::put_async"
1461 || path == "isahc::delete_async"
1462 || path == "isahc::send_async"
1463 || path.ends_with("HttpClient::new")
1464 || path.ends_with("HttpClientBuilder::build"))
1465 {
1466 return Some("Net");
1467 }
1468 // `reqwest::multipart::Part::file` (blocking/multipart.rs:223) opens the given path directly
1469 // (`std::fs::File::open`) to attach it as a form part — genuinely Fs, not Net (the eventual
1470 // upload is charged at `.send()`, already covered above).
1471 //
1472 // NOT modelled: `h3_client::pool::PoolClient::send_request` (async_impl/h3_client/pool.rs:207)
1473 // — `mod async_impl;` is PRIVATE in reqwest's lib.rs (only `self::async_impl::multipart` is
1474 // selectively re-exported), so `PoolClient` is unreachable from outside the crate; the real
1475 // HTTP/3 dispatch a consumer can actually reach is the already-covered `::send`/`::execute`.
1476 // Removed from open.tsv rather than guessed.
1477 // SOUNDNESS R337 — THE WRAPPER, not the wrapped. candor already charges
1478 // `native_tls_crate::Identity::from_pkcs8`/`from_pkcs12` `Fs` (the clause below), and the reason
1479 // it does is concrete rather than defensive: on the security-framework backend those create a
1480 // real `tempfile::TempDir` and write a keychain into it —
1481 // `native-tls-0.2.18/src/imp/security_framework.rs:93` (`identity.keychain`, `from_pkcs8`) and
1482 // `:167` (`tmp.keychain`, `from_pkcs12`). But candor classifies the call a CONSUMER writes, not
1483 // the dependency body behind it, so a consumer who reaches that code through its idiomatic
1484 // wrapper matched no rule at all — and in a CALIBRATED crate an unmatched path is a claim of
1485 // reviewed purity with no disclosure. A call taking only BYTES writes a file on macOS, and it
1486 // read pure.
1487 //
1488 // Swept for the CLASS rather than the two rows the coverage gate handed me: every caller of
1489 // `native_tls{,_crate}::Identity::from_pkcs8|from_pkcs12` in the local registry is reqwest
1490 // (0.11/0.12/0.13, all three spelling these the same way), lettre, and `tcp_stream` — which is
1491 // NOT calibrated, so its calls disclose rather than claim purity and it needs no rule.
1492 //
1493 // Suffix-matched, not exact-path, because all three reqwest majors carry the same names; and
1494 // narrow on purpose. The SIBLINGS are the fabrication control and they must stay pure:
1495 // `reqwest::tls::Identity::from_pem` is rustls-only, a `Cursor` over a buffer (tls.rs:373), and
1496 // lettre's `Certificate::from_der`/`from_pem` reach `native_tls::Certificate::*`, which imports
1497 // items with no keychain and no temp dir (security_framework.rs:206,212).
1498 // SOUNDNESS R342 — THE RESPONSE BODY IS READ FROM THE SOCKET, and `resp.text().await` is one of
1499 // the most-written lines in Rust networking. `send()` is charged; the methods that DRAIN what it
1500 // returned were not, so a helper that takes a `Response` and reads it — an extremely common
1501 // shape — was a positive purity claim on a CALIBRATED crate. Executed at reqwest 0.13.4:
1502 // `r.text()`, `r.json()` and `r.bytes()` all ABSENT while `c.get(..).send()` was `['Net']`.
1503 //
1504 // Verified in the source rather than assumed: `text` -> `text_with_charset` -> `bytes` ->
1505 // `do_bytes`, which is `BodyExt::collect(self.res.into_body()).await`
1506 // (`async_impl/response.rs:433`) — it drains the body off the wire. `chunk` and `bytes_stream`
1507 // are the streaming spellings of the same read; charging the stream CONSTRUCTOR follows this
1508 // file's own `Command::new` convention, where the effect is attributed at the point the caller
1509 // asks for it rather than at the poll nobody can see.
1510 //
1511 // Scoped by `Response::` and not by the bare verb: `::text` and `::json` are far too common a
1512 // leaf to match crate-wide. Found by the coverage gate the moment R185 taught the scanner to
1513 // see through `Option`/`Result` unwraps — the same "closing a silence makes consumers' gaps
1514 // visible" shape recorded on R302 and R337.
1515 if crate_name == "reqwest"
1516 && path.contains("Response::")
1517 && (path.ends_with("::text")
1518 || path.ends_with("::text_with_charset")
1519 || path.ends_with("::json")
1520 || path.ends_with("::bytes")
1521 || path.ends_with("::chunk")
1522 || path.ends_with("::bytes_stream")
1523 // SOUNDNESS R1528 (b10) — `blocking::Response::copy_to(w)` is the blocking drain of the same body
1524 // (EXECUTED: it blocked 1.5 s on a slow server and wrote the tail; a response built in memory: 0 ms).
1525 || path.ends_with("::copy_to"))
1526 {
1527 return Some("Net");
1528 }
1529 if crate_name == "reqwest" {
1530 if let Some(effect) = reqwest_r1528(path) {
1531 return Some(effect);
1532 }
1533 }
1534 if crate_name == "reqwest"
1535 && (path.ends_with("::Identity::from_pkcs12_der")
1536 || path.ends_with("::Identity::from_pkcs8_pem"))
1537 {
1538 return Some("Fs");
1539 }
1540 if crate_name == "reqwest" && path.ends_with("::Part::file") {
1541 return Some("Fs");
1542 }
1543 return None;
1544 }
1545 if crate_name == "ureq" && path.ends_with("::call") {
1546 return Some("Net");
1547 }
1548 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against ureq 2.12.1 (the version this ratchet's
1549 // rows were generated from — 3.x, also cached locally, rewrote this surface beyond recognition and
1550 // is a different question for a future pass). `::call` above is the no-body dispatch terminal;
1551 // these are its siblings:
1552 //
1553 // `Request::send`/`send_bytes`/`send_form`/`send_json`/`send_string` (request.rs:78-296) are the
1554 // WITH-BODY dispatch terminals — `do_call(Payload::..)`, the exact same terminal `call` reaches with
1555 // an empty payload. Missing them left every `agent.post(url).send_json(&body)` (the dominant idiom
1556 // for a JSON API call) silently pure.
1557 // `ureq::agent`/`request`/`request_url` (lib.rs:513,539,561) are real gaps in a stranger way: each
1558 // one's own body contains a REACHABLE (if `is_test(true)` was ever called — itself a `pub fn` a
1559 // consumer or another dependency could invoke) call to `testserver::test_agent()`, which binds a
1560 // real listening `TcpStream` (see `TestServer::new` below) — self-scan's whole-body reachability
1561 // treats that as real, and per this project's over-approximate-rather-than-miss discipline (a
1562 // toggleable global flag routing traffic through an internal listener is exactly the kind of path a
1563 // security-conscious caller wants disclosed, not silently pure), so does this rule. `agent()` is also
1564 // the crate's OWN base for `request`/`request_url` (`agent().request(..)`), and `request`/
1565 // `request_url` additionally carry the URL argument — matching the reqwest/isahc precedent above of
1566 // classifying the URL-bearing constructor `Net` too (idempotent with the eventual `.call()`/`.send()`)
1567 // so the destination is captured even if a caller never re-touches the returned `Request`.
1568 if crate_name == "ureq"
1569 && (path.ends_with("::send")
1570 || path.ends_with("::send_bytes")
1571 || path.ends_with("::send_form")
1572 || path.ends_with("::send_json")
1573 || path.ends_with("::send_string")
1574 || path == "ureq::agent"
1575 || path == "ureq::request"
1576 || path == "ureq::request_url"
1577 || path == "ureq::unversioned::transport::RustlsConnector::connect"
1578 || path == "ureq::tls::rustls::RustlsConnector::connect"
1579 || path == "ureq::RustlsConnector::connect")
1580 {
1581 return Some("Net");
1582 }
1583 // SOUNDNESS R1528 — ureq 3.4.2 (the version `open.tsv` was generated from; 3.x REWROTE the surface the
1584 // 2.12.1 rules above were written against). Every body READ in ureq-3.4.2 and every row EXECUTED against a
1585 // consumer fixture whose server is a 127.0.0.1 listener (the handoff REPORT). The partition:
1586 // * Net — the dispatch TERMINALS (`Agent::run` -> `run_via_middleware` -> `MiddlewareNext::handle` ->
1587 // `run::run` -> the pool's connector; `WithAgent::run`, `ConfigBuilder<RequestExtScope>::run`,
1588 // `RequestBuilder::send_empty` = `send`, free `ureq::run`), the resolver (`DefaultResolver::resolve`
1589 // = `to_socket_addrs`), and the connectors/transports that open or drive a socket
1590 // (`TcpConnector`/`DefaultConnector`/`ConnectProxyConnector`/`SocksConnector`/`NativeTlsConnector::
1591 // connect`, `TcpTransport::{transmit_output,await_input,is_open}`). A connector is charged although it
1592 // skips itself when its config does not apply (no proxy, chained transport already present): the rule
1593 // is "may", exactly as `RustlsConnector::connect` above.
1594 // * The free verbs `ureq::{get,post,put,delete,head,patch,options,trace,connect}` are request BUILDERS,
1595 // kept pure here by design (the deep engine charges the terminal `.call()`; candor-scan's
1596 // `scan_builder_entry_effect` table over-approximates the entry, and now also `options`/`trace`/
1597 // `connect`, which were silent siblings). Their real effect is the `Config::default` env read: R1555.
1598 // * Env — `Config::default` -> `Proxy::try_from_env` (`HTTP_PROXY`/`ALL_PROXY`/`NO_PROXY`), reached from
1599 // `Config::builder`, `Agent::config_builder`, `Agent::new_with_defaults` and `Proxy::try_from_env`
1600 // itself. EXECUTED: the agent's `config().proxy()` is `Some` only when `HTTP_PROXY` is set.
1601 // * Fs / Rand / Clock — `multipart::{Form::file,Part::file}` open the path (`File::open`), `Form::new`
1602 // draws its boundary from `getrandom::fill`, `send_body::File::as_body` is `metadata()` on the
1603 // caller's file, `transport::time::Instant::now` is `std::time::Instant::now`.
1604 // NOT claimed, and why (each filed or reviewed in the handoff REPORT): `ConfigBuilder::<setter>` and
1605 // `RequestScope::config` read the environment ONLY through `RequestBuilder::request_level_config` when the
1606 // builder already holds a URL error (request.rs:405-413) — conditional (R1553); `Either`/`Box<dyn
1607 // Transport>` forward to whatever transport they wrap and `ChainedConnector::connect` to whatever two
1608 // connectors it chains — the effect is the type argument's (R1554); `Agent::new_with_config`/`with_parts`
1609 // only box a connector and build a pool (REVIEWED_PURE_ENTRIES).
1610 if crate_name == "ureq" {
1611 // The free verbs `ureq::{get,post,...}` are deliberately NOT here: they only build a request (and read
1612 // the env, see R1555); candor-scan's `scan_builder_entry_effect` table over-approximates them as Net
1613 // for the syntactic scanner and a test pins them pure in THIS classifier for the deep engine.
1614 if path == "ureq::run" {
1615 return Some("Net");
1616 }
1617 if let Some((ty, m)) = type_method(
1618 path,
1619 "ureq",
1620 &["", "agent", "request", "request_ext", "config", "config::typestate", "middleware", "proxy",
1621 "unversioned::transport", "unversioned::transport::tcp", "unversioned::transport::connect",
1622 "unversioned::transport::socks", "unversioned::transport::time", "unversioned::resolver",
1623 "unversioned::multipart", "multipart", "tls::native_tls", "tls", "send_body"],
1624 ) {
1625 let hit = match (ty, m) {
1626 ("Agent", "run") | ("WithAgent", "run") | ("ConfigBuilder", "run")
1627 | ("RequestBuilder", "send_empty") | ("MiddlewareNext", "handle")
1628 | ("DefaultResolver", "resolve") => Some("Net"),
1629 ("TcpConnector" | "DefaultConnector" | "ConnectProxyConnector" | "SocksConnector"
1630 | "NativeTlsConnector", "connect") => Some("Net"),
1631 ("TcpTransport", "transmit_output" | "await_input" | "is_open") => Some("Net"),
1632 // `Agent::shutdown` (ureq 3.5.x; SOUNDNESS R1528 refresh, covgate lane, R1696): the consumer-callable route to the
1633 // `ShutdownHandle::shutdown` of every transport the agent registered, which `TcpStream::shutdown` implements with
1634 // `shutdown(2)` on the socket — the self-scan nominates `Unknown` alone for `Agent::shutdown` (a `dyn` handle),
1635 // so the row it DID nominate, `TcpStream::shutdown` (in a private module), has no consumer spelling. EXECUTED
1636 // (slow local HTTP server, a worker blocked in `read_to_string`): `agent.shutdown()` after 250 ms ended the
1637 // worker's read at 260 ms with an error; the same program without the call ran 811 ms to success. An agent with
1638 // no live transport shuts nothing down: charged on the call, like the other pool-level verbs.
1639 ("Agent", "shutdown") => Some("Net"),
1640 ("Config", "builder" | "default") | ("Agent", "config_builder" | "new_with_defaults")
1641 | ("Proxy", "try_from_env") => Some("Env"),
1642 ("Form", "file") | ("Part", "file") | ("File", "as_body") => Some("Fs"),
1643 ("Form", "new") => Some("Rand"),
1644 ("Instant", "now") => Some("Clock"),
1645 _ => None,
1646 };
1647 if hit.is_some() {
1648 return hit;
1649 }
1650 }
1651 }
1652 // NOT modelled (both removed from open.tsv, not guessed):
1653 // `ureq::Arc::connect` — the ratchet's generator strips generic arguments from an impl header, so
1654 // `impl TlsConnector for Arc<rustls::ClientConfig>` (rtls.rs:88) produced the type name "Arc" — a
1655 // bogus path (`ureq::Arc` names nothing; `Arc` is std's, not ureq's) that no consumer could ever
1656 // write. The trait method itself is real (ureq's own internal TLS dispatch, invoked by `.call()`,
1657 // already covered), but no FQN a real caller would spell resolves through this guess.
1658 // `ureq::TestServer::new` — `TestServer` (testserver.rs:62) lives in `mod testserver;` (lib.rs:444),
1659 // PRIVATE with no re-export, so `ureq::TestServer` is unreachable from outside the crate (the
1660 // listener it binds is real, but only `agent()`'s internal `is_test` branch above can reach it).
1661 // The `curl` crate (libcurl's safe binding — cargo's own HTTP client): the dispatch verbs are
1662 // `perform` (Easy/Easy2/Transfer/Multi), raw-socket `send`/`recv`, the keepalive `upkeep`, and the
1663 // multi-interface `action` (socket_action). The big setopt-style builder surface stays pure.
1664 // `Multi::timeout` is deliberately NOT matched: `Easy::timeout` is a pure CURLOPT_TIMEOUT setter
1665 // sharing the leaf — an under-report on the rare event-loop kick beats mis-tagging every consumer
1666 // that sets a timeout. (Consumer-side companion to the curl_* FFI tier, same A/B finding.)
1667 if crate_name == "curl"
1668 && (path.ends_with("::perform")
1669 || path.ends_with("::send")
1670 || path.ends_with("::recv")
1671 || path.ends_with("::upkeep")
1672 || path.ends_with("::action"))
1673 {
1674 return Some("Net");
1675 }
1676 // The modern async-HTTP / TLS / QUIC / DNS stack — the LAYER reqwest/ureq/isahc build on, and that
1677 // crates use DIRECTLY. Found by the independent-method differential on `oha` (2026-06-17): candor
1678 // honestly DISCLOSED these as blind but never CLASSIFIED them, leaving real Net reaches uncovered.
1679 // Verb-keyed (the pure type/builder/codec surface stays None) and CRATE-GATED, so generic verbs
1680 // (request/connect/get/read/write/accept) never fabricate across unrelated crates. Same precision
1681 // discipline as the reqwest/curl rules above; complements the scan_builder_entry_effect entries.
1682 match crate_name {
1683 // hyper 1.x client connection I/O (the builder/Body/Request types stay pure).
1684 "hyper" if path.ends_with("::send_request") || path.ends_with("::handshake") => return Some("Net"),
1685 // hyper-util's pooled legacy Client + its TCP connectors.
1686 "hyper_util" if path.ends_with("::request") || path.ends_with("::connect") => return Some("Net"),
1687 // hickory (trust-dns) resolver — issues DNS queries over the network.
1688 "hickory_resolver"
1689 if path.ends_with("::lookup_ip") || path.ends_with("::lookup") || path.ends_with("_lookup")
1690 || path.ends_with("::resolve") => return Some("Net"),
1691 // HTTP/3 over QUIC.
1692 "h3" if path.ends_with("::send_request") || path.ends_with("::recv_data")
1693 || path.ends_with("::recv_response") || path.ends_with("::send_data") => return Some("Net"),
1694 // QUIC transport (UDP socket send/recv): connection setup, datagrams, AND the stream byte I/O
1695 // (`RecvStream::read*` / `SendStream::write*` / `finish`). Opening a stream is caught above, but a
1696 // fn that only HOLDS a stream and reads/writes it would otherwise read silent-pure (review: a Net
1697 // under-report). Crate-gated to quinn, where these verbs are unambiguously the socket I/O.
1698 "quinn" if path.ends_with("::connect") || path.ends_with("::accept") || path.ends_with("::open_bi")
1699 || path.ends_with("::open_uni") || path.ends_with("::accept_bi") || path.ends_with("::accept_uni")
1700 || path.ends_with("::send_datagram") || path.ends_with("::read_datagram")
1701 || path.ends_with("::read") || path.ends_with("::read_chunk") || path.ends_with("::read_chunks")
1702 || path.ends_with("::read_to_end") || path.ends_with("::write") || path.ends_with("::write_all")
1703 || path.ends_with("::write_chunk") || path.ends_with("::write_chunks")
1704 || path.ends_with("::finish") => return Some("Net"),
1705 // TLS-over-TCP stream adapters — the actual socket handshake/I/O (the config/cert types stay pure).
1706 "tokio_rustls" | "native_tls"
1707 if path.ends_with("::connect") || path.ends_with("::accept") || path.ends_with("::handshake") =>
1708 return Some("Net"),
1709 // AF_VSOCK host<->guest sockets — inter-process / VM comms.
1710 "tokio_vsock" if path.ends_with("::connect") || path.ends_with("::bind") || path.ends_with("::accept") =>
1711 return Some("Ipc"),
1712 // Loads the OS trust store from disk (cert files / keychain).
1713 "rustls_native_certs" if path.ends_with("::load_native_certs") => return Some("Fs"),
1714 // `rlimit` reads/mutates the process's kernel resource limits — the closest bucket is Env (host/
1715 // process config); no dedicated process-state bucket exists, so getrlimit (read) and setrlimit
1716 // (mutate) share it. NOTE: `num_cpus::get`/`get_physical` are deliberately NOT modeled — asking the
1717 // OS for the CPU count is a near-pure topology query, and std's equivalent `thread::
1718 // available_parallelism` classifies pure; modeling it as Env would spray Env over every thread-pool
1719 // constructor (review: a high-noise over-report) for no capability a reviewer cares about.
1720 "rlimit" if path.ends_with("::getrlimit") || path.ends_with("::setrlimit")
1721 || path.ends_with("::increase_nofile_limit") => return Some("Env"),
1722 // rustls — the SYNC TLS core (tokio_rustls/native_tls above are the async/system adapters). The
1723 // record-layer I/O is `read_tls`/`write_tls` (pull/push raw bytes through a held `io::Read`/`Write`)
1724 // and `complete_io` (loops them until the handshake/buffers drain). The config/cert/builder types
1725 // (`ClientConfig`/`ServerConfig`/`ConfigBuilder`) are PURE. `process_new_packets` is deliberately
1726 // EXCLUDED — it only decrypts ALREADY-buffered bytes (no socket touch; docs say call it AFTER
1727 // read_tls), so flagging it would over-report Net on the pure decrypt step.
1728 "rustls" if path.ends_with("::read_tls") || path.ends_with("::write_tls")
1729 || path.ends_with("::complete_io") => return Some("Net"),
1730 // native-tls under its alternate crate name + the tokio async wrapper (the `native_tls` arm above
1731 // is the common name). The TLS handshake over a TcpStream is Net; the builder/cert types are pure.
1732 "native_tls_crate" | "tokio_native_tls"
1733 if path.ends_with("::connect") || path.ends_with("::accept")
1734 || path.ends_with("::handshake") => return Some("Net"),
1735 _ => {}
1736 }
1737 // THE COVERAGE-GATE SWEEP (2026-08-27) — two crate-root CONSTRUCTORS the verb-keyed match above
1738 // never reached because neither ends in connect/accept/handshake, verified against their real
1739 // per-platform impl (native-tls 0.2.18, rustls 0.23.43):
1740 //
1741 // `native_tls_crate::TlsConnector::new` (lib.rs:481, the PUBLIC newtype wrapper around the private
1742 // per-platform `imp::TlsConnector`) reads the system trust store — on the openssl backend it loads
1743 // the probed cert file/dir from disk before any socket exists. `Identity::from_pkcs8` (lib.rs:178,
1744 // wrapping `imp::Identity::from_pkcs8`) on the security-framework (macOS) backend creates a real
1745 // temporary keychain file on disk to import the PKCS8 key into.
1746 // `rustls::KeyLogFile::new` (key_log_file.rs:88) reads `$SSLKEYLOGFILE` and, if set, opens (creating
1747 // if needed) that file in append mode — a real, if opt-in, disk write path independent of the sync
1748 // TLS record-layer I/O the `rustls` arm above already covers.
1749 // SOUNDNESS R331 — …and `TlsConnector::builder`, which is the SAME trust-store read reached by the
1750 // other spelling. `TlsConnector::new` is literally `TlsConnector::builder().build()`
1751 // (`native-tls-0.2.18/src/lib.rs:481`), and `TlsConnectorBuilder::build` (`:451`) is where
1752 // `imp::TlsConnector::new` — the openssl-backend cert-file/dir load — actually runs. The builder
1753 // spelling is the DOMINANT idiom the moment any option is set, and it is what `reqwest` itself
1754 // writes (`reqwest-0.12.28/src/async_impl/client.rs:529`).
1755 //
1756 // **KEYED ON `builder`, NOT ON `build`, AND THAT IS THE WHOLE FIX.** The first attempt added
1757 // `ends_with("TlsConnectorBuilder::build")` and moved nothing, so the row was closed with the
1758 // conclusion that the scanner "never types `TlsConnector::builder()`" and the repair had to be
1759 // receiver typing. **That conclusion was wrong.** `.build()` is a method on a foreign-returned
1760 // receiver and cannot be typed without a return index for a foreign crate — but `TlsConnector::
1761 // builder()` is a PATH call, recorded and classified exactly like `TlsConnector::new` beside it.
1762 // Charging the constructor rather than the terminal is also this file's own convention
1763 // (`Command::new`). Over-charge direction: a `builder()` whose `.build()` never happens, which is
1764 // not a shape anyone writes.
1765 //
1766 // WHY IT LOOKED LIKE NOTHING WAS RECORDED: under the calibration exemption a call that classifies
1767 // `None` is DROPPED from the report, so "no calls recorded" is what a correct scan of an exempt
1768 // crate looks like — not evidence that the receiver went untyped. Measured: under the PLAIN
1769 // `native_tls::` spelling (not calibrated) the same body is PRESENT with `invisible: ["native_tls"]`,
1770 // a disclosure; under `native_tls_crate` (the calibrated rename reqwest and tungstenite use) it is
1771 // ABSENT with nothing hidden. The rename is what turns a disclosure into a purity claim.
1772 if matches!(crate_name, "native_tls" | "native_tls_crate")
1773 && (path.ends_with("TlsConnector::new")
1774 || path.ends_with("TlsConnector::builder")
1775 || path.ends_with("Identity::from_pkcs8")
1776 || path.ends_with("Identity::from_pkcs12"))
1777 {
1778 return Some("Fs");
1779 }
1780 if crate_name == "rustls" && path.ends_with("KeyLogFile::new") {
1781 return Some("Fs");
1782 }
1783 // THE COVERAGE-GATE REFRESH (2026-10-08): `KeyLogFile::log` (rustls-0.23.45 key_log_file.rs:100) is
1784 // `try_write` -> `file.write_all(..)` (:67) on the SSLKEYLOGFILE it opened.
1785 if crate_name == "rustls"
1786 && matches!(path, "rustls::KeyLogFile::log" | "rustls::key_log_file::KeyLogFile::log")
1787 {
1788 return Some("Fs");
1789 }
1790 // Message-queue clients fully encapsulate the socket (the underlying tokio::net lives
1791 // inside the crate, unseen), so a user's connect/publish/consume calls ARE the I/O
1792 // boundary — to a remote broker, hence Net. Match the broker round-trip verbs (snake_case
1793 // methods); the CamelCase option/property builders stay pure. (Found hardening on consumer
1794 // apps: lapin `basic_publish`/`queue_declare` and async-nats `publish`/`subscribe` were
1795 // classified pure — a message-queue client reporting no I/O.)
1796 if crate_name == "async_nats" {
1797 if path.ends_with("::connect")
1798 || path.contains("::publish")
1799 || path.ends_with("::subscribe")
1800 || path.ends_with("::queue_subscribe")
1801 || path.contains("::request")
1802 || path.ends_with("::flush")
1803 {
1804 return Some("Net");
1805 }
1806 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against async-nats 0.35.1 (the version this
1807 // ratchet's rows were generated from — 0.50.0, also cached locally, removed/relocated this
1808 // exact surface, a separate question for a future pass):
1809 //
1810 // `connect_with_options` (lib.rs:891) is the crate's OWN real entry point `connect()` calls one
1811 // hop down — the actual handshake, missing because the verb list above matches the bare
1812 // `::connect` suffix, not the `_with_options` sibling.
1813 // `ConnectOptions::credentials_file`/`with_credentials_file` (options.rs:417,438) load a real
1814 // credentials file off disk via `auth_utils::load_creds` before authenticating — Fs, not Net
1815 // (the network round-trip these feed happens later, at `connect`).
1816 // `ServerAddr::socket_addrs` (lib.rs:1473) does `tokio::net::lookup_host` — a real DNS query.
1817 if path == "async_nats::connect_with_options" {
1818 return Some("Net");
1819 }
1820 if path == "async_nats::ConnectOptions::credentials_file"
1821 || path == "async_nats::ConnectOptions::with_credentials_file"
1822 {
1823 return Some("Fs");
1824 }
1825 if path == "async_nats::ServerAddr::socket_addrs" {
1826 return Some("Net");
1827 }
1828 // SOUNDNESS R1528 — async-nats 0.50.0, the 105 grandfathered rows (above the 0.35.1 ceiling: a Cargo.lock
1829 // consumer got `invisible:[async_nats]`, a lockless one nothing). Every body was READ and EXECUTED against a
1830 // real nats-server with JetStream (docker `nats:latest -js`, monitoring port `/varz` read around each call;
1831 // handoff `rust-r1528/nats/`). See `async_nats_r1528`.
1832 if let Some(effect) = async_nats_r1528(path) {
1833 return Some(effect);
1834 }
1835 return None;
1836 }
1837 if crate_name == "lapin" {
1838 if path.ends_with("::connect")
1839 || path.ends_with("::create_channel")
1840 || path.contains("::basic_")
1841 || path.contains("::queue_")
1842 || path.contains("::exchange_")
1843 || path.contains("::tx_")
1844 || path.ends_with("::confirm_select")
1845 || path.ends_with("::close")
1846 {
1847 return Some("Net");
1848 }
1849 // SOUNDNESS R1528 — lapin 4.12.0 (above its 4.10.0 ceiling). See `lapin_r1528`.
1850 return lapin_r1528(path);
1851 }
1852 // SMTP email — lettre's `Transport::send` is the network dispatch; Message building is
1853 // pure. (Found hardening on a lettre consumer: `mailer.send(&email)` classified pure.)
1854 //
1855 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against lettre 0.11.23 source — the TLS-setup and
1856 // connection-establishment family this crate's own `send` rule never reached:
1857 //
1858 // `TlsParameters::new`/`new_rustls` (client/tls.rs:598,617) and `TlsParametersBuilder::build`/
1859 // `build_rustls` (tls.rs:325,446) — `build_rustls` calls `rustls_native_certs::load_native_certs()`
1860 // (a real OS-trust-store read) plus `tracing::debug!`; `build()` dispatches to it under the
1861 // (textually-visible, cfg-gated) `rustls` feature. `SmtpTransport`/`AsyncSmtpTransport::{from_url,
1862 // relay,starttls_relay}` (transport.rs:90,114,243; async_transport.rs:135,169,306) each construct a
1863 // `TlsParameters` directly (`relay`/`starttls_relay`) or via the crate's OWN `pub(crate)
1864 // from_connection_url` (`from_url`) — real TLS setup a consumer never spells `TlsParameters` to
1865 // reach. `FileTransport::read` (transport/file/mod.rs:216) calls `std::fs::read` directly.
1866 if crate_name == "lettre" {
1867 // SOUNDNESS R1597 — a transport's `send` does the TRANSPORT's effect, not the SMTP one. See `lettre_send_r1597`.
1868 if let Some(effect) = lettre_send_r1597(path) {
1869 return Some(effect);
1870 }
1871 // SOUNDNESS R1604 — `StubTransport` records the message in memory. See `lettre_stub_send_r1604`.
1872 if lettre_stub_send_r1604(path) {
1873 return None;
1874 }
1875 if path.ends_with("::send") || path.ends_with("::send_raw") {
1876 return Some("Net");
1877 }
1878 // SOUNDNESS R1528 — lettre 0.11.23, the message-building, DKIM and file rows. See `lettre_r1528`.
1879 if let Some(effect) = lettre_r1528(path) {
1880 return Some(effect);
1881 }
1882 // THE COVERAGE-GATE REFRESH (2026-10-08), read against lettre-0.11.23 source: the SMTP client
1883 // connection layer, which `send` above reaches but a consumer driving the protocol by hand never
1884 // names. `SmtpConnection::connect` -> `NetworkStream::connect` (connection.rs:66, a TCP dial);
1885 // `command` -> `write_all` + `flush` on the stream (:275); `read_response` -> `read_line` (:289);
1886 // `quit`/`test_connected`/`auth`/`message`/`message_iter`/`starttls` are `command` +
1887 // `read_response`; `abort` sends QUIT and `shutdown`s the socket (:178). The async twins
1888 // (async_connection.rs) are the same over tokio, `connect_with_transport` reading the greeting.
1889 // `AsyncNetworkStream::{connect_tokio1, upgrade_tls}` dial / handshake the socket (the sync
1890 // `NetworkStream` is in the private `mod net;` and nameable by no consumer — no rule, as before);
1891 // `Tokio1Executor::connect` is `AsyncSmtpConnection::connect_tokio1` (executor.rs:140). NOT charged:
1892 // the accessors (`server_info`, `has_broken`, `can_starttls`, `is_encrypted`, `peer_*`,
1893 // `certificate_chain`, `tls_verify_result`, `set_stream`).
1894 if let Some((ty, m)) = type_method(path, "lettre", &["", "transport::smtp::client"]) {
1895 const CONN: &[&str] = &["connect", "connect_tokio1", "connect_with_transport", "starttls", "quit",
1896 "abort", "test_connected", "auth", "message", "message_iter", "command", "read_response"];
1897 let hit = match ty {
1898 "SmtpConnection" | "AsyncSmtpConnection" => CONN.contains(&m),
1899 "AsyncNetworkStream" => matches!(m, "connect_tokio1" | "upgrade_tls"),
1900 "Tokio1Executor" => m == "connect",
1901 _ => false,
1902 };
1903 if hit {
1904 return Some("Net");
1905 }
1906 }
1907 // SOUNDNESS R337 — lettre's half of the same wrapper gap. `Identity::from_pem(pem, key)`
1908 // (transport/smtp/client/tls.rs:749) is `native_tls::Identity::from_pkcs8(pem, key)`, which on
1909 // macOS writes a temporary keychain. The `::Identity::` in the suffix is load-bearing: the
1910 // `Certificate::from_pem` twenty lines above it (`:676`) is in-memory and must stay pure, and a
1911 // bare `::from_pem` would charge both.
1912 if path.ends_with("::Identity::from_pem") {
1913 return Some("Fs");
1914 }
1915 if path == "lettre::TlsParametersBuilder::build"
1916 || path == "lettre::transport::smtp::client::TlsParametersBuilder::build"
1917 || path == "lettre::TlsParametersBuilder::build_rustls"
1918 || path == "lettre::transport::smtp::client::TlsParametersBuilder::build_rustls"
1919 || path == "lettre::TlsParameters::new"
1920 || path == "lettre::transport::smtp::client::TlsParameters::new"
1921 || path == "lettre::TlsParameters::new_rustls"
1922 || path == "lettre::transport::smtp::client::TlsParameters::new_rustls"
1923 // SOUNDNESS R331 — the `_native` half of the same family, which this enumeration never had.
1924 // It surfaced the moment `TlsConnector::builder` started charging: the coverage gate's
1925 // oracle saw `Fs` on `TlsParameters::new_native` and `TlsParametersBuilder::build_native`
1926 // and `classify()` had no rule for either. `build_native` is `TlsConnector::builder()`
1927 // (`lettre-0.11.23/src/transport/smtp/client/tls.rs:338`) and `new_native` delegates
1928 // straight to it (`:610`); the already-listed `build`/`new` reach whichever backend is
1929 // enabled and were covered, so the list held every variant EXCEPT the one named after the
1930 // backend that does the disk read.
1931 //
1932 // `build_boring`/`new_boring` are deliberately NOT added. `build_boring` (`:384`) builds a
1933 // `boring::ssl::SslConnector`, and openssl's `SslConnector::builder` calls
1934 // `set_default_verify_paths()` — so it is PROBABLY `Fs` too, but `boring` is not a crate
1935 // this file has read, and the last three defects in this area came from charging a path
1936 // whose body nobody had opened. Left for a pass that reads it.
1937 || path == "lettre::TlsParameters::new_native"
1938 || path == "lettre::transport::smtp::client::TlsParameters::new_native"
1939 || path == "lettre::TlsParametersBuilder::build_native"
1940 || path == "lettre::transport::smtp::client::TlsParametersBuilder::build_native"
1941 || path == "lettre::SmtpTransport::from_url"
1942 || path == "lettre::SmtpTransport::relay"
1943 || path == "lettre::SmtpTransport::starttls_relay"
1944 || path == "lettre::AsyncSmtpTransport::from_url"
1945 || path == "lettre::AsyncSmtpTransport::relay"
1946 || path == "lettre::AsyncSmtpTransport::starttls_relay"
1947 || path == "lettre::FileTransport::read"
1948 {
1949 return Some("Fs");
1950 }
1951 // `AsyncStd1Executor::{connect,fs_read,fs_write}` (executor.rs:223,260,265, the sealed
1952 // `Executor` trait impl — `#[doc(hidden)]` in lettre's own source, so this is a narrower, less-
1953 // visited surface than the others above, but genuinely reachable: both the trait and the type
1954 // are `pub`, re-exported at the crate root) dial a real connection / read+write real files.
1955 // `AsyncSmtpConnection`/`AsyncNetworkStream::connect_asyncstd1` (client/async_connection.rs:121,
1956 // client/async_net.rs:207) dial a real `async-std` TCP connection — reachable only via the
1957 // module-qualified path (`transport::smtp::client::`), since neither type is re-exported at the
1958 // crate root (unlike `TlsParameters`'s sibling forms, which at least reach `client::` — the dual
1959 // spellings above, same shape as rusqlite's `Blob`/`Backup`).
1960 if path == "lettre::AsyncStd1Executor::connect"
1961 || path == "lettre::AsyncSmtpConnection::connect_asyncstd1"
1962 || path == "lettre::transport::smtp::client::AsyncSmtpConnection::connect_asyncstd1"
1963 || path == "lettre::AsyncNetworkStream::connect_asyncstd1"
1964 || path == "lettre::transport::smtp::client::AsyncNetworkStream::connect_asyncstd1"
1965 {
1966 return Some("Net");
1967 }
1968 if path == "lettre::AsyncStd1Executor::fs_read" || path == "lettre::AsyncStd1Executor::fs_write" {
1969 return Some("Fs");
1970 }
1971 // NOT included: `NetworkStream::{shutdown,set_read_timeout,set_write_timeout}` (client/net.rs)
1972 // LOOK like three more hits (self-scan flags them too, propagating from the coarse
1973 // `std::net::TcpStream` whole-handle rule) but `mod net;` (client/mod.rs:50) is PRIVATE and
1974 // `NetworkStream` is never re-exported (only imported crate-internally via a bare `use
1975 // self::net::NetworkStream;`, mod.rs:35) — no external consumer can ever name the type, so no
1976 // rule was added despite self-scan flagging it (the same `InnerConnection`/`RawStatement` shape
1977 // as the rusqlite fix elsewhere in this file).
1978 return None;
1979 }
1980 // WebSockets — tungstenite (the modern successor to the old `websocket` crate). connect
1981 // and the socket read/write/send are network; Message constructors are pure. (Found on a
1982 // tungstenite consumer: connect + send + read classified pure.)
1983 //
1984 // THE FIX: `connect` only names the URL-dialing client entry point. tungstenite ALSO ships the
1985 // stream-first client/server handshake free functions — `client`/`client_with_config` (client.rs:176,
1986 // 152: "use this if you need a nonblocking handshake ... or a custom stream") and
1987 // `accept`/`accept_with_config`/`accept_hdr`/`accept_hdr_with_config` (server.rs:23-63) — each of
1988 // which does `{Client,Server}Handshake::start(stream, ..).handshake()`, the REAL WS upgrade
1989 // handshake read/write over an already-open stream. These are the documented way to run tungstenite
1990 // over a caller-managed TCP/TLS/mio stream (exactly the shape most async runtimes use, since
1991 // tungstenite itself is sync/transport-agnostic) — missing from a verb list keyed only on the
1992 // dial-it-yourself `connect` spelling. Same vein as `git2::Submodule::clone`/`ignore::Walk::new`: an
1993 // established effect's SIBLING entry point absent from the allowlist.
1994 if crate_name == "tungstenite" {
1995 // SOUNDNESS R1528 (b10) — the grandfathered rows the verb list below never reached. See `tungstenite_r1528`.
1996 if let Some(effect) = tungstenite_r1528(path) {
1997 return Some(effect);
1998 }
1999 if path.ends_with("::connect")
2000 || path.ends_with("::connect_with_config")
2001 || path.ends_with("::client")
2002 || path.ends_with("::client_with_config")
2003 || path.ends_with("::client_tls")
2004 || path.ends_with("::client_tls_with_config")
2005 || path.ends_with("::accept")
2006 || path.ends_with("::accept_with_config")
2007 || path.ends_with("::accept_hdr")
2008 || path.ends_with("::accept_hdr_with_config")
2009 || path.ends_with("::read")
2010 || path.ends_with("::write")
2011 || path.ends_with("::send")
2012 || path.ends_with("::close")
2013 || path.ends_with("::flush")
2014 || path.ends_with("::read_message")
2015 || path.ends_with("::write_message")
2016 {
2017 // SOUNDNESS R1604 — `WebSocket<S>` / `WebSocketContext` I/O does whatever `S` does, and classify never sees `S` (the
2018 // scan strips type arguments). Over a `Cursor` it performs no syscall (EXECUTED); over a `TcpStream` or the
2019 // `MaybeTlsStream` that `connect` returns it writes the socket (EXECUTED). Kept `Net` for every `S`: an over-charge on
2020 // an in-memory stream instead of a silence on a socket.
2021 return Some("Net");
2022 }
2023 return None;
2024 }
2025 // elasticsearch: request builders are pure; only the `.send()` dispatch is HTTP I/O
2026 // (same shape as reqwest / the AWS SDK). (Found on an elasticsearch consumer.)
2027 // SOUNDNESS R342 — …and the RESPONSE BODY, one wrapper up from reqwest's. `Response::json` is
2028 // `self.response.json::<B>().await` (`http/response.rs:102`) — reqwest's body drain, which this file
2029 // now charges — and `text`/`bytes`/`exception` are the same read. A consumer writing
2030 // `client.search(..).send().await?.json().await?` had the `send` charged and the read silent.
2031 //
2032 // This is EXACTLY the distinction this file's own exclusion list already draws for this type:
2033 // `Response::content_type` is documented there as a pure accessor reading a header off an
2034 // ALREADY-received response. `status_code`/`headers` are the same shape and stay pure; the body
2035 // methods are not accessors, they pull the remaining bytes off the wire. Both halves are asserted
2036 // in the test so the line between them cannot move by accident.
2037 if crate_name == "elasticsearch"
2038 && path.contains("Response::")
2039 && (path.ends_with("::json")
2040 || path.ends_with("::text")
2041 || path.ends_with("::bytes")
2042 || path.ends_with("::exception"))
2043 {
2044 return Some("Net");
2045 }
2046 if crate_name == "elasticsearch" && path.ends_with("::send") {
2047 return Some("Net");
2048 }
2049 // gRPC — tonic. The transport connect and the Grpc client RPC dispatch are network;
2050 // codecs and request/response wrappers are pure. (connect repro-confirmed on a consumer;
2051 // the unary/streaming RPC verbs are from the tonic::client::Grpc API.)
2052 //
2053 // THE COVERAGE-GATE FIX: the rule above only ever covered the CLIENT half. `Router::serve`/
2054 // `::serve_with_shutdown` (transport/server/mod.rs:783,811) are the SERVER's own most common entry
2055 // points — a tonic quickstart's last line — and both bind a real listening socket via
2056 // `TcpIncoming::new` (incoming.rs:197: `StdTcpListener::bind(addr)`), the crate's other bare-`pub`
2057 // constructor (`Server<L>::serve_with_shutdown` at mod.rs:530 is a DIFFERENT, `pub(crate)` method on
2058 // a different receiver — not reachable, and not this one). A `deny Net` on a gRPC SERVER app missed
2059 // its own listen call entirely.
2060 if crate_name == "tonic" {
2061 if path.ends_with("::connect")
2062 || path.ends_with("::unary")
2063 || path.ends_with("::server_streaming")
2064 || path.ends_with("::client_streaming")
2065 || path.ends_with("::streaming")
2066 || path.ends_with("::Router::serve")
2067 || path.ends_with("::Router::serve_with_shutdown")
2068 || path == "tonic::TcpIncoming::new"
2069 || path == "tonic::transport::server::TcpIncoming::new"
2070 {
2071 return Some("Net");
2072 }
2073 // SOUNDNESS R1528 — tonic 0.14.6 (above its 0.12.3 ceiling). See `tonic_r1528`.
2074 return tonic_r1528(path);
2075 }
2076 // Kafka — rdkafka (FFI to librdkafka). Producer send + consumer poll/recv/subscribe/
2077 // commit are network round-trips to the brokers. (API-calibrated + unit-tested; a real
2078 // repro needs librdkafka/cmake, deferred.)
2079 if crate_name == "rdkafka" {
2080 if path.ends_with("::send")
2081 || path.ends_with("::send_result")
2082 || path.ends_with("::recv")
2083 || path.ends_with("::poll")
2084 || path.ends_with("::subscribe")
2085 || path.ends_with("::commit")
2086 || path.ends_with("::commit_message")
2087 || path.ends_with("::commit_consumer_state")
2088 || path.ends_with("::store_offset")
2089 || path.ends_with("::seek")
2090 || path.ends_with("::fetch_metadata")
2091 || path.ends_with("::fetch_watermarks")
2092 // SOUNDNESS R1528 — `fetch_group_list` (`Client`, `BaseConsumer`, `StreamConsumer`, and the
2093 // `Consumer` trait method; client.rs:509 is the one body, the others delegate) calls
2094 // `rd_kafka_list_groups`, a blocking broker request like `fetch_metadata` above. EXECUTED
2095 // 2026-11-08: against a local listener the call sent 41 bytes to the broker address. The
2096 // self-scan oracle said only `Log` (a trace event) — the Net is what a `deny Net` gate needs.
2097 || path.ends_with("::fetch_group_list")
2098 || path.ends_with("::flush")
2099 {
2100 return Some("Net");
2101 }
2102 // SOUNDNESS R1528 — rdkafka 0.39.0, the grandfathered rows above the 0.39.0 ceiling are not the point here
2103 // (the ceiling IS 0.39.0); these are the broker-touching verbs the suffix list cannot see. See
2104 // `rdkafka_r1528`.
2105 return rdkafka_r1528(path);
2106 }
2107 // cap-std: capability-oriented std. I/O goes *through* a held capability handle
2108 // (Dir/Pool/Clock/...), so these calls ARE the effect. Recognising them means a
2109 // cap-std project's real I/O is detected and matches the capability it declared
2110 // (via `declared_caps`/`capstd_cap`) — conformance against unforgeable capabilities.
2111 if crate_name.starts_with("cap_") {
2112 if path.contains("::net::Unix") || path.contains("::os::") {
2113 return Some("Ipc");
2114 }
2115 if path.contains("::net") {
2116 return Some("Net");
2117 }
2118 if path.contains("::time") {
2119 return Some("Clock");
2120 }
2121 if path.contains("::fs") || crate_name == "cap_tempfile" || crate_name == "cap_directories" {
2122 return Some("Fs");
2123 }
2124 return None;
2125 }
2126 // Local IPC (Unix-domain sockets) is I/O but not *network* — keep it distinct so
2127 // CANDOR_NO_AMBIENT and audits don't conflate it with internet access. async-std puts its
2128 // Unix sockets under `os::unix::net` (mirroring std); async-net (smol's net layer) under
2129 // `unix`.
2130 if path.starts_with("tokio::net::Unix")
2131 || path.starts_with("std::os::unix::net")
2132 || path.starts_with("async_std::os::unix::net")
2133 || path.starts_with("async_net::unix")
2134 {
2135 return Some("Ipc");
2136 }
2137 // Raw packet capture / raw sockets — libpnet (the dominant low-level networking crate; powers
2138 // bandwhich, sniffers, custom-protocol tools). `datalink::channel` opens an L2 socket and
2139 // `transport::transport_channel` an L3/L4 raw socket — both ARE network I/O. Packet construction
2140 // (pnet_packet / pnet_base, MacAddr, Ethernet frames…) is pure and stays unclassified. The actual
2141 // frame read/write happens via methods on the returned Sender/Receiver (trait-object dispatch the
2142 // syntactic backend can't resolve), so the channel-open call is the precise Net boundary. (Found
2143 // scanning bandwhich — a packet sniffer — which reported Net 0.)
2144 if crate_name == "pnet" || crate_name == "pnet_datalink" || crate_name == "pnet_transport" {
2145 if path.ends_with("::channel") || path.ends_with("::transport_channel") {
2146 return Some("Net");
2147 }
2148 return None;
2149 }
2150 // Directory traversal — `ignore` (BurntSushi's gitignore-aware walker; powers ripgrep, fd). The walk
2151 // EXECUTORS read the directory tree from disk = Fs. Type-precise on purpose: the configuration builders
2152 // (`OverrideBuilder::build`, `GitignoreBuilder::build`, the `WalkBuilder` setters) and `DirEntry`
2153 // accessors are PURE — only `WalkBuilder::build`/`build_parallel` (which kick off the walk) and
2154 // `WalkParallel::run` (which drives it) touch the filesystem. A bare `build` would wrongly flag the
2155 // config builders. (Found scanning fd — a file finder — which reported Fs 2: its own `fs::read_dir`
2156 // was caught, but the `ignore`-based traversal that IS fd was invisible cross-crate.)
2157 //
2158 // `Walk::new`/`Walk::from_iter` are the crate's own documented convenience constructors — literally
2159 // `WalkBuilder::new(path).build()` / `WalkBuilder::from_iter(paths).build()` in `ignore`'s own source
2160 // (walk.rs:1128-1146) — but a verb list keyed only on `WalkBuilder::build`/`build_parallel`/
2161 // `WalkParallel::run`/`add_ignore` never sees `Walk::new(root)` (the crate's own top-level doc example
2162 // and the SAME shape as the already-fixed `walkdir::WalkDir::new`/`git2::Repository::clone`: a
2163 // documented public entry point that reaches the modelled effect through a wrapper the allowlist
2164 // didn't name). `deny Fs` over `for e in Walk::new(root) { .. }` exited 0. Charged at the SAME
2165 // construction site as `WalkBuilder::build` — no receiver typing needed, it's a plain `Expr::Call` on
2166 // `ignore::Walk::new`/`ignore::Walk::from_iter`, crate-gated so an unrelated `Walk::new` (a different
2167 // crate, or a local type) cannot match this arm.
2168 if crate_name == "ignore" {
2169 if path == "ignore::WalkBuilder::build"
2170 || path == "ignore::WalkBuilder::build_parallel"
2171 || path.ends_with("::WalkParallel::run")
2172 // `add_ignore(path)` LOOKS like a config setter but reads that ignore file from disk at call
2173 // time (it returns the read error) — unlike the pure `add_custom_ignore_filename(name)` which
2174 // only stores a filename string. The lone Fs-touching builder method in the otherwise-pure setter
2175 // surface, so it was silently pure under the covered-crate floor.
2176 || path == "ignore::WalkBuilder::add_ignore"
2177 || path == "ignore::Walk::new"
2178 || path == "ignore::Walk::from_iter"
2179 {
2180 return Some("Fs");
2181 }
2182 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against ignore 0.4.33 — the gitignore-matcher
2183 // construction family, a DIFFERENT public surface from the walk-builder family above:
2184 //
2185 // `Gitignore::new` (gitignore.rs:103) opens and reads the given file directly via
2186 // `GitignoreBuilder::add`. `Gitignore::global` (gitignore.rs:141) reads `$PWD` then delegates to
2187 // `GitignoreBuilder::new(cwd).build_global()`. `GitignoreBuilder::add` (gitignore.rs:405) is the
2188 // crate's own per-file loader — `File::open` + line-by-line read — the same shape `add_ignore`
2189 // above was fixed for, on the SIBLING type. `GitignoreBuilder::build_global`/
2190 // `gitconfig_excludes_path` (gitignore.rs:377,583) resolve git's global excludesfile: read
2191 // `GIT_CONFIG_GLOBAL`/`XDG_CONFIG_HOME`/`HOME` env vars, then open and parse whichever gitconfig
2192 // file they point at. `WalkBuilder::build_matchers` (walk.rs:675) is the incremental-walk
2193 // sibling of the already-covered `build_parallel` — same `self.build_ignore()` call, different
2194 // caller-facing verb. `IncrementalIgnore::matched`/`matched_with_errors` (incremental.rs:194,213)
2195 // are the newer per-directory incremental-walk API: each lazily loads that directory's ignore
2196 // files (and, when `max_filesize` is set, `stat`s the candidate file) as the walk descends.
2197 //
2198 // `Gitignore`/`GitignoreBuilder`/`gitconfig_excludes_path` live in `pub mod gitignore;`
2199 // (lib.rs:59) but — unlike `Walk`/`WalkBuilder` two lines up (`pub use crate::walk::{..}`,
2200 // lib.rs:52) — are NOT re-exported at the crate root, so the only real, compilable spelling is
2201 // the module-qualified one (proven by a consumer fixture: `ignore::Gitignore::new` does not
2202 // exist, `ignore::gitignore::Gitignore::new` does). `IncrementalIgnore` IS re-exported at the
2203 // root (`pub use crate::incremental::{..}`, lib.rs:51), so the bare form is correct there.
2204 if path == "ignore::gitignore::Gitignore::new"
2205 || path == "ignore::gitignore::Gitignore::global"
2206 || path == "ignore::gitignore::GitignoreBuilder::add"
2207 || path == "ignore::gitignore::GitignoreBuilder::build_global"
2208 || path == "ignore::gitignore::gitconfig_excludes_path"
2209 || path == "ignore::WalkBuilder::build_matchers"
2210 || path == "ignore::IncrementalIgnore::matched"
2211 || path == "ignore::IncrementalIgnore::matched_with_errors"
2212 || path == "ignore::DirEntry::metadata"
2213 || path == "ignore::walk::DirEntry::metadata"
2214 || path == "ignore::WalkParallel::visit"
2215 || path == "ignore::walk::WalkParallel::visit"
2216 {
2217 return Some("Fs");
2218 }
2219 return None;
2220 }
2221 // Filesystem watching — `notify` (the de-facto fs-watch crate: watchexec, cargo-watch, mdbook). A
2222 // watcher opens an OS notification handle (inotify / FSEvents / kqueue / ReadDirectoryChanges) and
2223 // registers paths — observing filesystem state changes = Fs. The lifecycle boundary: any
2224 // `*Watcher::new` constructor (RecommendedWatcher/PollWatcher/INotifyWatcher/FsEventWatcher/…), the
2225 // `recommended_watcher` convenience fn, and the `watch`/`unwatch` registration verbs. `Config`/`Event`/
2226 // `EventKind` data types stay pure. (Found scanning watchexec: its watcher-`create` read Fs 0.)
2227 if crate_name == "notify" {
2228 if path.ends_with("Watcher::new")
2229 || path.ends_with("::recommended_watcher")
2230 || path.ends_with("::watch")
2231 || path.ends_with("::unwatch")
2232 // `ReadDirectoryChangesWatcher::create` (windows.rs:475, `pub use windows::
2233 // ReadDirectoryChangesWatcher` at the crate root) is a SECOND, directly-callable
2234 // constructor distinct from the `Watcher::new` trait method above — it takes a raw
2235 // meta-event channel `Watcher::new`'s impl builds internally by calling THIS same
2236 // `create`, then spawns the real ReadDirectoryChangesW watch server thread.
2237 || path.ends_with("ReadDirectoryChangesWatcher::create")
2238 {
2239 return Some("Fs");
2240 }
2241 return None;
2242 }
2243 // std DNS resolution — `("host", 80).to_socket_addrs()` / `std::net::lookup_host("host")` perform a
2244 // real getaddrinfo query (Net), but the classify table covered only the socket I/O *types*, so they
2245 // floored silently (sweep [37]; the syntactic engine modelled DNS only at the libc layer).
2246 if path.ends_with("::to_socket_addrs")
2247 || path == "std::net::lookup_host"
2248 || path.ends_with("ToSocketAddrs::to_socket_addrs")
2249 {
2250 return Some("Net");
2251 }
2252 // Raw sockets. Match the I/O *types* only — `std::net` also holds pure data types
2253 // (SocketAddr, IpAddr, …) whose construction must NOT be flagged.
2254 if path.starts_with("std::net::TcpStream")
2255 || path.starts_with("std::net::TcpListener")
2256 || path.starts_with("std::net::UdpSocket")
2257 || path.starts_with("tokio::net::")
2258 {
2259 // …but the PURE accessors read back local/option state — no network I/O — so the whole-type Net
2260 // rule fabricated Net on them (sweep [24], the precision failure; mirrors the arboard/memmap2 accessor
2261 // carve-outs). local_addr/peer_addr return bound/connected addresses; nodelay/ttl/take_error read
2262 // socket options/state. Every genuine verb (connect/read/write/send/recv/accept) stays Net.
2263 if path.ends_with("::local_addr")
2264 || path.ends_with("::peer_addr")
2265 || path.ends_with("::nodelay")
2266 || path.ends_with("::ttl")
2267 || path.ends_with("::take_error")
2268 {
2269 return None;
2270 }
2271 return Some("Net");
2272 }
2273 // Legacy tokio 0.1 socket crates — `tokio_tcp`/`tokio_udp` are *entirely* networking
2274 // (no pure types to over-flag), so the whole crate is Net. (Found hardening on websocat,
2275 // which is still on tokio 0.1: its `tokio_tcp::TcpStream::connect` was classified
2276 // network-free — a network tool confidently reporting 0 Net.)
2277 if matches!(crate_name, "tokio_tcp" | "tokio_udp") {
2278 return Some("Net");
2279 }
2280 // The other async runtimes mirror tokio's module layout, and their `net` modules hold only
2281 // socket I/O types (the pure `SocketAddr`/`IpAddr` are re-exports that resolve to `std::net`,
2282 // so they're excluded by def-path). `mio` is the low-level non-blocking-socket layer under
2283 // tokio/others; `async_net` is smol's net crate. Closes the async-std/smol/mio gap the
2284 // tokio_tcp note flagged. (Calibrated by module structure — these crates ARE networking — not
2285 // a live repro; the TCP/UDP types are defined in-crate so the def-path prefix is exact.)
2286 if path.starts_with("async_std::net::")
2287 || path.starts_with("mio::net::")
2288 || crate_name == "async_net"
2289 {
2290 return Some("Net");
2291 }
2292 // Database clients. Like the AWS/HTTP builders, only the execution verbs are I/O;
2293 // query *construction* is pure. Best-effort across crates (tune via CANDOR_CONFIG).
2294 // Note: bare `::query` is deliberately omitted — it executes in postgres/rusqlite but
2295 // only *builds* in sqlx, so including it would false-positive sqlx's `query()` builder.
2296 if DB_CRATES.contains(&crate_name) {
2297 // SOUNDNESS R1528 — sqlx-core 0.9.0's pool/migrate rows, reached by a `sqlx` facade consumer under the
2298 // facade's own spelling (`sqlx::PgPool::begin_with`, `sqlx::pool::PoolConnection::close`). See `sqlx_r1528`.
2299 if crate_name == "sqlx" {
2300 if let Some(effect) = sqlx_r1528("sqlx", path) {
2301 return Some(effect);
2302 }
2303 }
2304 // Postgres / SQLite-family clients: `query`/`batch_execute`/`prepare`/etc. ARE the
2305 // execution (round-trips to the server). sqlx is the outlier where bare `query()`
2306 // only BUILDS — it keeps the narrow set below. (Found by running on a real
2307 // tokio-postgres app, pgman: candor had reported only 4 of ~20 DB call sites.)
2308 if matches!(crate_name, "postgres" | "tokio_postgres" | "deadpool_postgres" | "rusqlite") {
2309 // `PG_DB_VERBS`, hoisted to module level so the AS-EFF-008 masking guard reads the SAME
2310 // table (SOUNDNESS R806; see `masks_locator` and `PG_DB_VERBS_NO_SQL`).
2311 const PG: &[&str] = PG_DB_VERBS;
2312 if PG.iter().any(|v| path.ends_with(v)) {
2313 return Some("Db");
2314 }
2315 // THE COVERAGE-GATE SWEEP (2026-08-27): `CancelToken::cancel_query` (tokio-postgres
2316 // cancel_token.rs:34) opens a BRAND NEW connection to the server to send a raw CancelRequest
2317 // packet — real socket I/O, but not a query round-trip on an existing connection like the PG
2318 // verb list above, so bucketed `Net` rather than `Db`.
2319 if matches!(crate_name, "tokio_postgres" | "postgres")
2320 && path.ends_with("CancelToken::cancel_query")
2321 {
2322 return Some("Net");
2323 }
2324 // SOUNDNESS R1528 — the SAME connection-opening cancel, reached through its two DEPRECATED
2325 // forwarders. `Client::cancel_query` (tokio-postgres client.rs:740) is `self.cancel_token()
2326 // .cancel_query(tls).await` and `Transaction::cancel_query` (transaction.rs:299) forwards to the
2327 // client's: both end in `cancel_query.rs`, which calls `connect_socket` (a fresh TcpStream/
2328 // UnixStream) and writes the CancelRequest packet. EXECUTED 2026-11-08 against a local socket
2329 // that logged what arrived: `c.cancel_query(NoTls)` made a SECOND connection and sent a
2330 // CancelRequest (code 80877102). Net, like `CancelToken::cancel_query` above. `postgres` has no
2331 // `Client::cancel_query` (only `cancel_token`), so this is tokio_postgres only. The `_raw`
2332 // variants write to a caller-supplied stream and are not claimed.
2333 if crate_name == "tokio_postgres"
2334 && (path.ends_with("::Client::cancel_query")
2335 || path.ends_with("::Transaction::cancel_query"))
2336 {
2337 return Some("Net");
2338 }
2339 // SOUNDNESS R1528 — `postgres::Client::is_valid` (postgres-0.19.14 client.rs:550) sends
2340 // `simple_query("")` and awaits the reply under a timeout: a real round-trip on the connection.
2341 // EXECUTED: the server saw a Query message. Db, as `simple_query` is in the verb list above.
2342 // `postgres` only — tokio_postgres has no `is_valid`.
2343 if crate_name == "postgres" && path.ends_with("::Client::is_valid") {
2344 return Some("Db");
2345 }
2346 // SOUNDNESS R1618 — `deadpool_postgres::Pool::{get, timeout_get}` is the pool's connection-opening verb. EXECUTED
2347 // (macOS, DYLD interposer, postgres:16): a COLD pool's `get()` = connect(127.0.0.1:port) + 1 getentropy (the SCRAM
2348 // nonce) + the startup/auth exchange; a WARM pool's `get()` (an idle object, default `RecyclingMethod::Fast`) = 0
2349 // connects and 0 entropy. `Db` is the primary because the cold arm opens a database session, and a `Verified`/`Clean`
2350 // recycling method sends a query on the warm arm too. The path is `deadpool_postgres::Pool::get`: `Pool` is a
2351 // `deadpool::managed::Pool<Manager>` alias, which the scan keeps under the consumer's own spelling. Keyed on the exact
2352 // head so another crate's `Pool::get` (and `deadpool::managed::Pool::get` over a caller's own manager) is not claimed.
2353 if crate_name == "deadpool_postgres"
2354 && matches!(path, "deadpool_postgres::Pool::get" | "deadpool_postgres::Pool::timeout_get")
2355 {
2356 return Some("Db");
2357 }
2358 // SOUNDNESS R1618 — `build_transaction().start()` is `BEGIN`. EXECUTED: 1 simple_query record and a BEGIN on the wire
2359 // (tokio_postgres, postgres, deadpool). The scan types the builder chain `c.build_transaction().start()` as the
2360 // RECEIVER's type, so the consumer's path is `tokio_postgres::Client::start` (neither `Client` nor `Object` has a
2361 // `start` of its own, so the name can only come from the builder), and a held builder is `TransactionBuilder::start`.
2362 // `isolation_level` / `read_only` / `deferrable` / `build_transaction` itself send nothing (EXECUTED, 0 events).
2363 if let Some((head, "start")) = path.rsplit_once("::") {
2364 if matches!(
2365 head,
2366 "tokio_postgres::Client" | "tokio_postgres::TransactionBuilder"
2367 | "postgres::Client" | "postgres::TransactionBuilder"
2368 | "deadpool_postgres::Client" | "deadpool_postgres::Object" | "deadpool_postgres::TransactionBuilder"
2369 ) {
2370 return Some("Db");
2371 }
2372 }
2373 // rusqlite only: opening the database IS the connection establishment (`Connection::
2374 // open`/`open_in_memory`/`open_with_flags` — the embedded analog of `::connect`).
2375 //
2376 // THE FIX: rusqlite 0.32's `Connection` type has SIX `open*` constructors, not three —
2377 // `open_with_flags_and_vfs` and `open_in_memory_with_flags_and_vfs` call the real
2378 // `InnerConnection::open_with_flags` (the sqlite3_open_v2 FFI) directly (lib.rs:492,530), and
2379 // `open_in_memory_with_flags` (lib.rs:515) calls `Connection::open_with_flags(":memory:", ..)`
2380 // — but an exact-suffix allowlist keyed on the THREE simplest names never matches any of
2381 // them (`"open_in_memory_with_flags".ends_with("::open_with_flags")` is false: the suffix
2382 // check needs the string to END in that literal tail, and `_and_vfs`/`_with_flags` add
2383 // characters AFTER it). The same shape as `ignore::Walk::new`: a sibling spelling of an
2384 // already-modelled constructor missing from the allowlist, silently pure because rusqlite is
2385 // calibrated. Match on the `Connection::open` PREFIX rather than enumerating every suffix —
2386 // every current and future `Connection::open*` constructor opens a real handle. Scoped to the
2387 // `Connection::` segment specifically (not a bare leaf prefix) so an unrelated same-crate
2388 // `open`-prefixed method on a DIFFERENT type (e.g. the private `pragma::Sql::open_brace`,
2389 // which pushes one char to a string buffer — no I/O) cannot be swept in by accident.
2390 //
2391 // Same sighting, different constructor: `Connection::blob_open` and `Blob::reopen` call
2392 // `ffi::sqlite3_blob_open`/`sqlite3_blob_reopen` directly (blob/mod.rs:218,251) — genuinely
2393 // Db, and `sqlite3_blob_open`/`sqlite3_blob_reopen` are already in this file's own FFI-leaf DB
2394 // table above (matched only when a caller names the raw `ffi::` leaf directly) — but rusqlite's
2395 // own documented safe wrapper, the incremental-BLOB-I/O API every rusqlite blob consumer
2396 // actually calls, carried no rule of its own.
2397 if crate_name == "rusqlite"
2398 && (path.contains("::Connection::open")
2399 || path.ends_with("::Connection::blob_open")
2400 || path.ends_with("::Blob::reopen"))
2401 {
2402 return Some("Db");
2403 }
2404 // THE COVERAGE-GATE SWEEP (2026-08-27): same shape again, and the SAME root cause as
2405 // `git2`'s — this `if crate_name == "rusqlite"` block returns unconditionally, so it never
2406 // reaches this file's own `sqlite3_*` FFI-leaf table below even though every one of these
2407 // calls a leaf ALREADY listed there (`sqlite3_backup_init`/`_step`, `sqlite3_blob_read`/
2408 // `_write`). Verified against rusqlite 0.32.1 source. `Backup::new`/`new_with_names` are the
2409 // crate's own online-backup constructors (backup.rs:187,200); `step`/`run_to_completion`
2410 // drive it; `Connection::backup`/`restore` (backup.rs:61,99) are the one-call convenience
2411 // wrappers around the same API and need their OWN rule since a consumer calling THEM never
2412 // names `Backup` at all. `Blob::{read,write}_at[_exact]`/`raw_read_at[_exact]`/
2413 // `write_all_at` (blob/pos_io.rs) are the incremental-BLOB positional-I/O methods, each
2414 // calling `ffi::sqlite3_blob_read`/`_write` directly. `Connection::from_handle`/
2415 // `_owned`/`extension_init2` and the free fn `init_auto_extension` (lib.rs:950,999,966;
2416 // auto_extension.rs:26) wrap a caller-supplied raw `*mut ffi::sqlite3` into a live
2417 // `Connection` — the loadable-extension entry points, same "produces a live connection"
2418 // effect as `Connection::open`, just over a handle the caller already has instead of one
2419 // this call opens itself (the `tokio_postgres::connect_raw`/`sea_orm::connect_proxy` shape).
2420 //
2421 // NOT included: `InnerConnection::*` and `RawStatement::step` LOOK like six more hits (self-
2422 // scan flags them too) but `mod inner_connection;`/`mod raw_statement;` (lib.rs:119,127) are
2423 // PRIVATE — despite `pub struct InnerConnection`/`RawStatement`, neither type has any public
2424 // path a real consumer could ever name or obtain a value of, so no rule was added for them
2425 // (a rule would be dead weight, never reachable). `Context::get_connection` (functions.rs)
2426 // is ALSO left out, deliberately: it returns a `ConnectionRef` accessor to an
2427 // ALREADY-established connection (`ffi::sqlite3_context_db_handle`, not an I/O verb) — the
2428 // same "handle accessor, not a syscall" shape as `TcpStream::local_addr`, not confirmed
2429 // enough to charge without risking the over-charge this file's history has repeatedly warned
2430 // against. THE COVERAGE-GATE SWEEP (2026-08-27) resolved this from "left in the ratchet"
2431 // to a formal call: `REVIEWED_PURE_ENTRIES` now carries `("rusqlite",
2432 // "rusqlite::Context::get_connection")` so the gate stops re-flagging it every refresh.
2433 //
2434 // TWO SPELLINGS EACH for `Backup`/`Blob`/`init_auto_extension`: none of the three is
2435 // re-exported at rusqlite's crate root (unlike `Connection`, declared directly in lib.rs), so
2436 // the ONLY real spelling a consumer can write is the module-qualified one
2437 // (`rusqlite::backup::Backup::new`, `rusqlite::blob::Blob::read_at`,
2438 // `rusqlite::auto_extension::init_auto_extension`) — but the coverage-gate ratchet recorded
2439 // the SHORT, technically-unreachable guess (`generate.py` always adds a bare
2440 // `{crate}::{Type}::{fn}` guess as a possible root-alias, without confirming a `pub use`
2441 // actually exists for it). Both are listed: the short form so this fix closes the exact row
2442 // the ratchet carries, the long form so it fires for what a real consumer's source (proven
2443 // against a compiling fixture) actually contains — dropping the long form would leave the
2444 // real gap open while the bookkeeping said closed.
2445 if crate_name == "rusqlite"
2446 && (path == "rusqlite::Backup::new"
2447 || path == "rusqlite::backup::Backup::new"
2448 || path == "rusqlite::Backup::new_with_names"
2449 || path == "rusqlite::backup::Backup::new_with_names"
2450 || path == "rusqlite::Backup::step"
2451 || path == "rusqlite::backup::Backup::step"
2452 || path == "rusqlite::Backup::run_to_completion"
2453 || path == "rusqlite::backup::Backup::run_to_completion"
2454 || path == "rusqlite::Connection::backup"
2455 || path == "rusqlite::Connection::restore"
2456 || path == "rusqlite::Blob::read_at"
2457 || path == "rusqlite::blob::Blob::read_at"
2458 || path == "rusqlite::Blob::read_at_exact"
2459 || path == "rusqlite::blob::Blob::read_at_exact"
2460 || path == "rusqlite::Blob::raw_read_at"
2461 || path == "rusqlite::blob::Blob::raw_read_at"
2462 || path == "rusqlite::Blob::raw_read_at_exact"
2463 || path == "rusqlite::blob::Blob::raw_read_at_exact"
2464 || path == "rusqlite::Blob::write_at"
2465 || path == "rusqlite::blob::Blob::write_at"
2466 || path == "rusqlite::Blob::write_all_at"
2467 || path == "rusqlite::blob::Blob::write_all_at"
2468 || path == "rusqlite::Connection::from_handle"
2469 || path == "rusqlite::Connection::from_handle_owned"
2470 || path == "rusqlite::Connection::extension_init2"
2471 || path == "rusqlite::init_auto_extension"
2472 || path == "rusqlite::auto_extension::init_auto_extension"
2473 || path == "rusqlite::Statement::insert"
2474 || path == "rusqlite::statement::Statement::insert"
2475 || path == "rusqlite::Statement::raw_execute"
2476 || path == "rusqlite::statement::Statement::raw_execute"
2477 // SOUNDNESS R342 — `Rows::next` STEPS the statement: it is `self.advance()?`
2478 // (`rusqlite-0.39.0/src/row.rs:40`), i.e. `sqlite3_step`, which is where a query's
2479 // rows actually come from. Iterating a result set is the ordinary way to read from
2480 // sqlite and it carried no `Db`. `advance` is listed beside it because it is `pub`
2481 // and is the same call one level down.
2482 || path == "rusqlite::Rows::next"
2483 || path == "rusqlite::row::Rows::next"
2484 || path == "rusqlite::Rows::advance"
2485 || path == "rusqlite::row::Rows::advance")
2486 {
2487 return Some("Db");
2488 }
2489 // THE COVERAGE-GATE REFRESH (2026-10-08), read against rusqlite-0.39.0 source. Transactions,
2490 // savepoints and pragmas are SQL round-trips through `execute_batch`/`query_row`/`prepare`,
2491 // which this block already charges, but a consumer calling the wrapper never names them:
2492 // * `Transaction::new`/`new_unchecked` -> `conn.execute_batch("BEGIN …")` (transaction.rs:125);
2493 // `commit`/`rollback`/`finish` -> `execute_batch("COMMIT"/"ROLLBACK")` (:192,:204,:219);
2494 // `savepoint`/`savepoint_with_name` -> `Savepoint::with_name_` -> `execute_batch("SAVEPOINT …")` (:253).
2495 // * `Savepoint::{new, with_name, savepoint, savepoint_with_name}` (same `SAVEPOINT`), `commit`
2496 // (`RELEASE`, :314), `rollback` (`ROLLBACK TO`, :328), `finish` (:342).
2497 // * `Connection::{transaction_with_behavior, unchecked_transaction, savepoint,
2498 // savepoint_with_name}` (:427,:465,:495,:507) construct the two above.
2499 // * `Connection::pragma_query_value`/`pragma_update_and_check` -> `query_row` (pragma.rs:154,:262),
2500 // `pragma_query`/`pragma` -> `prepare` + rows (:172,:209), `pragma_update` -> `execute_batch` (:238).
2501 // NOT charged: `drop_behavior`/`set_drop_behavior` (a field), and the private `pragma::Sql` builder.
2502 if crate_name == "rusqlite" {
2503 if let Some((ty, m)) = type_method(path, "rusqlite", &["", "transaction", "pragma"]) {
2504 let hit = match ty {
2505 "Transaction" => matches!(m, "new" | "new_unchecked" | "commit" | "rollback" | "finish"
2506 | "savepoint" | "savepoint_with_name"),
2507 "Savepoint" => matches!(m, "new" | "with_name" | "savepoint" | "savepoint_with_name"
2508 | "commit" | "rollback" | "finish"),
2509 "Connection" => matches!(m, "transaction_with_behavior" | "unchecked_transaction"
2510 | "savepoint" | "savepoint_with_name" | "pragma" | "pragma_query"
2511 | "pragma_query_value" | "pragma_update" | "pragma_update_and_check"),
2512 _ => false,
2513 };
2514 if hit {
2515 return Some("Db");
2516 }
2517 }
2518 }
2519 // SOUNDNESS R1528 — the 17 reachable `eval/coverage-gate/open.tsv` rusqlite rows that this
2520 // block's `return None` left silent at every consumer (the 18th, `query_row_and_then`, is a
2521 // `PG_DB_VERBS` member now: it takes SQL text at argument 0, so the masking guard must read it
2522 // too). Each body READ in rusqlite-0.39.0 and each ends in a `sqlite3_*` leaf that the FFI table
2523 // below ALREADY charges `Db` (its R166 membership criterion: database content, or the
2524 // process-global extension registry); a consumer never names that leaf, which is the whole
2525 // silence. All EXECUTED 2026-11-09 against a consumer fixture (see the handoff REPORT):
2526 // * `Connection::close` -> `sqlite3_close` (lib.rs:803; inner_connection.rs:157): a WAL database's
2527 // `-wal`/`-shm` files were gone after it. `cache_flush` -> `sqlite3_db_cacheflush`
2528 // (inner_connection.rs:320): the file grew mid-transaction only after it.
2529 // * `Statement::exists` -> `Rows::next` -> `sqlite3_step` (statement.rs:349).
2530 // * `Connection::serialize`/`deserialize`/`deserialize_bytes`/`deserialize_read_exact` ->
2531 // `sqlite3_serialize`/`sqlite3_deserialize` (serialize.rs): the in-memory image read out / a
2532 // database swapped in. (`deserialize_read_exact` also reads the CALLER's reader, which is
2533 // that reader's own effect, not claimed here.)
2534 // * `Connection::load_extension` -> `sqlite3_load_extension` (inner_connection.rs:193): the
2535 // extension's init ran. `load_extension_enable`/`_disable` and `LoadExtensionGuard::new`
2536 // (which is `load_extension_enable`) -> `sqlite3_enable_load_extension`: SQL `load_extension()`
2537 // was refused before and worked after.
2538 // * `Wal::checkpoint`/`checkpoint_v2` (hooks/mod.rs:481,485) -> `sqlite3_wal_checkpoint[_v2]`.
2539 // * `register_/cancel_/reset_auto_extension` (auto_extension.rs:50-62) -> the PROCESS-GLOBAL
2540 // registry: a later `Connection::open` ran the registered routine.
2541 // NOT claimed (accessors/pure neighbours that stay None): `is_autocommit`, `changes`,
2542 // `last_insert_rowid`, `is_readonly`, `Wal::name`, `init_auto_extension` (its own rule above).
2543 if let Some((ty, m)) = type_method(path, "rusqlite", &["", "serialize", "hooks", "statement",
2544 "load_extension_guard"])
2545 {
2546 let hit = match ty {
2547 "Connection" => matches!(m, "close" | "cache_flush" | "serialize" | "deserialize"
2548 | "deserialize_bytes" | "deserialize_read_exact" | "load_extension"
2549 | "load_extension_enable" | "load_extension_disable"),
2550 "Statement" => m == "exists",
2551 "LoadExtensionGuard" => m == "new",
2552 "Wal" => matches!(m, "checkpoint" | "checkpoint_v2"),
2553 _ => false,
2554 };
2555 if hit {
2556 return Some("Db");
2557 }
2558 }
2559 if matches!(
2560 path,
2561 "rusqlite::register_auto_extension" | "rusqlite::auto_extension::register_auto_extension"
2562 | "rusqlite::cancel_auto_extension" | "rusqlite::auto_extension::cancel_auto_extension"
2563 | "rusqlite::reset_auto_extension" | "rusqlite::auto_extension::reset_auto_extension"
2564 ) {
2565 return Some("Db");
2566 }
2567 return None;
2568 }
2569 // redis: the way redis is ACTUALLY used is the high-level `Commands`/`AsyncCommands`
2570 // traits (`con.get`/`set`/`hset`/`lpush`/…) — every method is a round-trip — plus
2571 // connection establishment. The shared VERBS below only catch the low-level
2572 // `cmd("GET").query(con)`, so without this a normal redis user's calls classify as
2573 // PURE. (Found hardening on redis-rs: a fn doing `con.get`/`set` reported no effects.)
2574 // SOUNDNESS R335 — the SENTINEL module, which the rules above reach only by accident. redis's
2575 // `sentinel` surface is how a client finds the current master in a failover deployment, and
2576 // every lookup TALKS TO THE SENTINEL SERVERS: `Sentinel::master_for` runs
2577 // `find_master_address` (a `SENTINEL masters` round-trip) and its own doc says "this will
2578 // connect to the master node to verify that it considers itself master"; `get_sentinel_client`
2579 // opens a client per address and issues `ROLE` until one answers. Measured on redis 1.6.0 —
2580 // which is this crate's REVIEWED CEILING, so the calibration exemption applied and every one of
2581 // them read as a positive purity claim, in both the receiver-typed and UFCS spellings.
2582 //
2583 // A DENYLIST, not the enumeration the surrounding rules use, and that is the point. The
2584 // enumerate-the-connecting-verbs shape is what goes stale: a release that ADDS a lookup verb
2585 // adds a SILENCE, the cardinal direction. Inverted, the whole module is `Db` except its
2586 // config surface, so a new verb is charged by default and only a new PURE verb costs a
2587 // fabrication. The exempt set is not guessed: every `pub fn` leaf in redis-1.6.0/src/sentinel.rs
2588 // is one of 33 names, and `build` / `new` / `set_*` are exactly the ones that touch no socket
2589 // (`Sentinel::build` parses URLs via `into_connection_info`; `SentinelClientBuilder::build`
2590 // assembles a config struct). The `contains("::Sentinel")` arm is not the loose match R321
2591 // punished: the module's ENTIRE public surface was enumerated before writing it, and no item
2592 // named `Sentinel*` exists anywhere else in the crate, nor any of these verb names.
2593 if crate_name == "redis"
2594 && (path.starts_with("redis::sentinel::") || path.contains("::Sentinel"))
2595 {
2596 let m = path.rsplit("::").next().unwrap_or(path);
2597 // SOUNDNESS R1528 (redis 1.7.1): `build` is NOT pure either: `Sentinel::build_inner` seeds
2598 // `replica_start_index` with `random_replica_index` -> `rand::rng().random_range(..)` (sentinel.rs:284,752),
2599 // and `SentinelClient::build` / `SentinelClientBuilder::build` end in it (EXECUTED: 1-2 OS entropy reads on a
2600 // fresh thread, 0 on an idle one). It is still NOT charged, and the reason is the measurement that stopped
2601 // the rule: `let mut s = Sentinel::build(u)?; s.master_for(..)` has an untyped `s` (the `?` result), so a
2602 // `Rand` rule on `build` REPLACED the lock arm's `invisible:[redis]` with `['Rand']` and the `Db` of
2603 // `master_for` reached no rule (the scan reported `Rand` alone, on both `Sentinel::build` and
2604 // `SentinelClient::build` followed by `get_connection`). That deletes the only disclosure of the real
2605 // effect for a secondary one — not a resolution. Filed as R1590:silent.
2606 if m == "build" || m == "new" || m.starts_with("set_") {
2607 return None;
2608 }
2609 // §E1 HIT COUNTER (`CANDOR_ALIAS_DEBUG`) for `bin/corpus-ab.py --mark`.
2610 if candor_report::env::trace("CANDOR_ALIAS_DEBUG") {
2611 eprintln!("R335SENTINEL {path}");
2612 }
2613 return Some("Db");
2614 }
2615 // THE COVERAGE-GATE REFRESH (2026-10-08), read against redis-1.7.1 source. The sync `Connection`'s
2616 // raw protocol pair and the sync `PubSub` reader touch the socket directly:
2617 // `send_packed_command` -> `send_bytes` -> `reader.write_all(bytes)` (connection.rs:1661,:1053);
2618 // `recv_response` -> `read(true)` -> `parser.parse_value(reader)` (:1668,:1850);
2619 // `PubSub::get_message` -> `self.con.read(false)` (:2204). `Net`, not the `Db` the command verbs
2620 // carry: these move bytes, they issue no command. Exact type segment (`type_method`), so
2621 // `MultiplexedConnection`/`ClusterConnection` are not swept in.
2622 if crate_name == "redis" {
2623 if let Some((ty, m)) = type_method(path, "redis", &["", "connection"]) {
2624 if (ty == "Connection" && matches!(m, "send_packed_command" | "recv_response"))
2625 || (ty == "PubSub" && m == "get_message")
2626 {
2627 return Some("Net");
2628 }
2629 }
2630 }
2631 // SOUNDNESS R1528 — redis 1.7.1, the grandfathered rows above the 1.6.0 ceiling. See `redis_r1528`.
2632 if crate_name == "redis" {
2633 if let Some(effect) = redis_r1528(path) {
2634 return Some(effect);
2635 }
2636 }
2637 if crate_name == "redis"
2638 && (path.contains("Commands::")
2639 // THE COVERAGE-GATE SWEEP (2026-08-27), a pre-existing over-report found in passing:
2640 // `path.contains("::get_connection")` is a SUBSTRING match, so it also matched
2641 // `Client::get_connection_info` (client.rs:78) — a pure accessor returning an
2642 // already-stored `&ConnectionInfo`, no round-trip at all. Verified against redis 1.6.0.
2643 || (path.contains("::get_connection") && !path.ends_with("::get_connection_info"))
2644 || path.contains("::get_async_connection")
2645 || path.contains("::get_multiplexed_async_connection")
2646 // a live `ConnectionManager` round-trips (Db), but `ConnectionManagerConfig` is a pure
2647 // in-memory builder (set_number_of_retries/set_max_delay) — exclude it (adversarial review).
2648 // `ConnectionManager::clone` is an Arc refcount bump — no Db round-trip (sweep [27]).
2649 || (path.contains("ConnectionManager") && !path.contains("ConnectionManagerConfig")
2650 && !path.ends_with("::clone"))
2651 || path.ends_with("::query")
2652 || path.ends_with("::query_async")
2653 || path.ends_with("::req_command")
2654 || path.ends_with("::req_packed_command")
2655 || path.ends_with("::req_packed_commands"))
2656 {
2657 return Some("Db");
2658 }
2659 // mongodb: a document-store API with none of the SQL verbs — the user calls
2660 // `coll.find_one`/`insert_one`/`aggregate`/… and `Client::with_uri_str`. Without
2661 // these a mongodb user's calls classify PURE. (Found hardening: a fn doing
2662 // `find_one`+`insert_one` reported no effects.) Handle accessors (name/namespace)
2663 // and option/doc builders don't match these verbs, so they stay pure.
2664 //
2665 // THE FIX: `Client::with_options(options)` IS `with_uri_str`'s own body one call down —
2666 // `with_uri_str` (client.rs:179) is literally `ClientOptions::parse(uri).await?;
2667 // Client::with_options(options)`, and `with_options` (client.rs:188) is where the topology/
2668 // monitoring actually spins up. A caller who already holds a `ClientOptions` (built
2669 // programmatically, or via `ClientOptions::parse` called separately) uses `with_options`
2670 // directly — mongodb's OWN alternate entry point for the identical effect, missing from an
2671 // allowlist keyed only on the URI-string spelling. Same shape as `ignore::Walk::new` and
2672 // diesel's `establish`: a sibling constructor of an already-modelled effect. Verified against
2673 // mongodb 3.8.1 source for both the async (`client.rs`) and sync (`sync/client.rs`) `Client`.
2674 if crate_name == "mongodb" {
2675 const MONGO: [&str; 28] = [
2676 "::with_uri_str", "::with_options", "::connect", "::find", "::find_one", "::insert_one",
2677 "::insert_many", "::update_one", "::update_many", "::delete_one",
2678 "::delete_many", "::replace_one", "::aggregate", "::count_documents",
2679 "::estimated_document_count", "::count", "::distinct", "::run_command",
2680 "::find_one_and_update", "::find_one_and_delete", "::find_one_and_replace",
2681 "::list_collections", "::list_collection_names", "::list_databases",
2682 "::list_database_names", "::create_collection", "::create_index", "::watch",
2683 ];
2684 if MONGO.iter().any(|v| path.ends_with(v)) {
2685 return Some("Db");
2686 }
2687 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against mongodb 3.8.1 — the client-side
2688 // field-level encryption (CSFLE) surface, entirely unrelated to the CRUD verb list above.
2689 //
2690 // `ClientEncryption::decrypt` (client/csfle/client_encryption.rs:195) is an ordinary `pub
2691 // async fn` that bottoms out in `CryptExecutor::run_ctx` (client/csfle/state_machine.rs:106),
2692 // a state-machine loop that — depending on state — runs a real `list_collections` query
2693 // against the key-vault database, executes a real command against `mongocryptd` (respawning
2694 // the child process if the connection drops), and fetches data keys from the key vault:
2695 // genuinely Net (and, via mongocryptd respawn, Exec), reached from a manual decrypt call, not
2696 // a guess. Suffix-matched because the real path runs through a re-export hop the ratchet's
2697 // crate-root-alias guess skips: `ClientEncryption` is defined inside `pub(crate) mod csfle;`
2698 // (client.rs:4) and reachable only via `pub use crate::client::csfle::client_encryption;` at
2699 // the crate root (lib.rs:74) — `mongodb::client_encryption::ClientEncryption`, not bare
2700 // `mongodb::ClientEncryption`. The name is specific enough within this crate that a suffix
2701 // match carries no fabrication risk (crate-gated, and `ClientEncryption` exists nowhere else
2702 // in mongodb's own source).
2703 if path.ends_with("ClientEncryption::decrypt") {
2704 return Some("Net");
2705 }
2706 // NOT modelled (removed from open.tsv, not guessed): `CreateDataKey::execute` /
2707 // `Encrypt::execute` — a DIFFERENT false-guess shape from every other row in this pass.
2708 // `CreateDataKey`/`Encrypt` (action/csfle/create_data_key.rs, action/csfle/encrypt.rs) are
2709 // the crate's real, publicly re-exported Action-builder types (`mongodb::action::csfle::
2710 // {CreateDataKey,Encrypt}`) — but the SAME effect self-scan found (the real `run_ctx` call)
2711 // lives in `impl Action for CreateDataKey<'a> { async fn execute(self) -> .. { .. } }`
2712 // (client/csfle/client_encryption/create_data_key.rs:14), which is the INPUT to the
2713 // `#[action_impl]` proc macro, not a real method: the `Action` trait (action.rs:104) declares
2714 // only `optional`, and its own doc comment says the crate's action types are "executed via
2715 // `await` (or `run` if using the sync client)" — the macro consumes this `execute` fn body
2716 // and re-emits it as `IntoFuture::into_future`, so no compiled `CreateDataKey`/`Encrypt`
2717 // value ever has a method literally named `execute` a consumer could call. Self-scan found a
2718 // real function performing a real effect; it is just never reachable under this spelling
2719 // (nor, by construction, under ANY spelling — a consumer's call site is `.await`, not a
2720 // `.execute()` method call, so candor-scan would need to resolve the `IntoFuture` desugaring
2721 // itself to see this at all, a different, deeper question than a classify() rule can answer).
2722 //
2723 // SOUNDNESS R1528 (mongodb lane, 3.9.1): the effect is not unreachable, it is reached through the
2724 // CREATOR the consumer does call (`coll.drop()`, `session.commit_transaction()`, ...) and charged
2725 // there; see `mongodb_r1528`, which also recognises the macro-input spellings so the generator's rows
2726 // are covered. The `in-use-encryption` rows stay unclaimed (R1585).
2727 return mongodb_r1528(path);
2728 }
2729 // mysql / mysql_async: the `query`/`exec` families + `get_conn`/`ping` execute
2730 // immediately — no build-then-execute split like sqlx, so matching `::query` is safe
2731 // here. Same DB-verb-dialect gap class as redis/mongodb; calibrated from the Queryable
2732 // API (unit-tested; a real-app repro is the remaining confirmation).
2733 //
2734 // THE FIX: `Conn::new(opts)` is each crate's OWN primary connection constructor — not a
2735 // pool helper like `get_conn`, but the raw handle. `mysql::Conn::new` (conn/mod.rs:342) calls
2736 // `conn.connect_stream()?; conn.connect()?` directly; `mysql_async::Conn::new` (conn/mod.rs:921)
2737 // returns a future that does the same handshake. Real connection establishment, same shape as
2738 // diesel's `establish`/rusqlite's `open*` — but `::new` never appeared in this verb list, so a
2739 // bare `Conn::new(opts)?` (the crate's OWN first doctest example) read pure. Scoped to the
2740 // `Conn::` segment specifically (`::new` alone is far too generic a suffix to key on blindly —
2741 // `Opts`/`Pool`/query-result types in the same two crates have their own pure `new`s).
2742 if matches!(crate_name, "mysql" | "mysql_async") {
2743 if path.ends_with("::Conn::new") {
2744 return Some("Db");
2745 }
2746 const MY: [&str; 16] = [
2747 "::query", "::query_first", "::query_iter", "::query_map", "::query_fold",
2748 "::query_drop", "::exec", "::exec_first", "::exec_iter", "::exec_map",
2749 "::exec_fold", "::exec_drop", "::exec_batch", "::prep", "::ping", "::get_conn",
2750 ];
2751 if MY.iter().any(|v| path.ends_with(v)) {
2752 return Some("Db");
2753 }
2754 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against mysql_async 0.37.0:
2755 // `Conn::from_url` (conn/mod.rs:1059) is `Conn::new(Opts::from_str(url)?)` one hop down —
2756 // the crate's OWN alternate entry point for the identical connect effect `Conn::new` above
2757 // already carries, same shape as `ignore::Walk::new`/diesel's `establish`.
2758 // `WhiteListFsHandler::handle` (local_infile_handler/builtin.rs:58, the `GlobalHandler` impl
2759 // for `LOAD DATA LOCAL INFILE`) opens a real file off the caller-supplied whitelist —
2760 // reachable at the crate root (`pub use self::local_infile_handler::{builtin::
2761 // WhiteListFsHandler, ..}`, lib.rs:500).
2762 if path == "mysql_async::Conn::from_url" {
2763 return Some("Db");
2764 }
2765 if path.ends_with("WhiteListFsHandler::handle") {
2766 return Some("Fs");
2767 }
2768 // NOT modelled (all three removed from open.tsv, not guessed): `mysql::MyTcpBuilder::connect`/
2769 // `Stream::connect_tcp`/`Stream::make_secure` (mysql, not mysql_async — io/tcp.rs, io/mod.rs,
2770 // io/tls/native_tls_io.rs). `mod io;` is PRIVATE in mysql's lib.rs with no re-export of
2771 // `MyTcpBuilder`/`Stream`, so none of the three is a path any external consumer can name —
2772 // the crate's real, externally-reachable connect effect is `Conn::new` above (already
2773 // covered), which calls into this private `io` module internally. Also NOT modelled:
2774 // `mysql_async::PathOrBuf::read` (opts/mod.rs) — `PathOrBuf` is declared in the same private
2775 // `mod opts;` whose OTHER types (`Opts`, `OptsBuilder`, …) ARE individually `pub use`-
2776 // re-exported at the crate root (lib.rs:493) but `PathOrBuf` specifically is not, so it too
2777 // is unreachable — the same private-module shape as tempfile's `imp::` functions above.
2778 // (R1528, 0.37.1: it IS reachable as a VALUE a public signature hands out, but no spelling of it can be
2779 // typed by the scan, so no rule can fire: filed R1586.)
2780 if crate_name == "mysql" {
2781 if let Some(effect) = mysql_r1528(path) {
2782 return Some(effect);
2783 }
2784 }
2785 return mysql_async_r1528(path);
2786 }
2787 // sea_orm: an ORM whose execution is split from building (like sqlx). The query
2788 // BUILDERS (`Entity::find`, `Entity::insert`) are pure; execution happens at `.all`/
2789 // `.one`/`.count`/`.stream` and `Insert/Update/Delete::exec`. The write path via an
2790 // ActiveModel (`model.insert(db)`) executes too — distinguished from the `EntityTrait`
2791 // builder by the trait in the path (`ActiveModelTrait::`). (Found hardening on a
2792 // sea_orm consumer app: `.all(db)` reads and `ActiveModel::insert` writes were pure.)
2793 if crate_name == "sea_orm" {
2794 // sea_orm RE-EXPORTS sea_query (`sea_orm::sea_query::…`), whose builder algebra collides with
2795 // the execution verbs: `Func::count(col)` builds a COUNT() expr, `Condition::all()` AND-groups
2796 // filters, `Expr::count(…)` — all PURE, none touch a db. The `::all`/`::count`/`::one` execution
2797 // rule fabricated Db on them (sweep [5]). sea_query is pure query construction end-to-end, so
2798 // exclude the whole re-exported namespace first.
2799 if path.contains("sea_query") {
2800 return None;
2801 }
2802 // SOUNDNESS R1605 — the crate-root spellings of the same builders (`sea_orm::Condition::all`,
2803 // `sea_orm::entity::prelude::Expr::{all,count}`, `sea_orm::ExprTrait::count`) still match the verbs below and are
2804 // charged `Db` for pure work (EXECUTED: 0 statements). They are NOT excluded yet: with an entity declared in the
2805 // consumer crate, `Entity::find().filter(Condition::all()..).all(db)` reached no other rule (the derive-generated
2806 // `Entity` left `.all(db)` untyped), so that charge was the only `Db` on code that runs a SELECT (EXECUTED).
2807 // `candor-scan` now types that terminal (R1605: the derive's `Entity`, a generic `E: EntityTrait`, a let-bound
2808 // builder chain from the lock-pinned `EntityTrait` declaration; R1606: a chain rooted at a generic or a local
2809 // implementor is never typed through a local leaf record, and its steps are walked by what the source says each
2810 // returns). The exclusion is STILL held. Re-measured over 76 sea_orm consumer crates x {lock, lockless} on top of
2811 // R1606/R1607: it drops `Db` on 487 rows; an independent token checker over each row's call closure leaves 47
2812 // whose closure runs a query no typed terminal or on-site hedge covers, and it is BLIND to a dropped local edge
2813 // (cakedome `association::util::read_in`, whose `find.all()` binds to the chain root `RepoHandle` instead of the
2814 // declared `Find` — SOUNDNESS R1608), so its 312 "pure" rows are not a proof. See SOUNDNESS R1606.
2815 if path.ends_with("::all")
2816 || path.ends_with("::one")
2817 || path.ends_with("::count")
2818 || path.ends_with("::stream")
2819 || path.ends_with("::exec")
2820 || path.ends_with("::exec_with_returning")
2821 || path.ends_with("::exec_without_returning")
2822 || path.ends_with("::connect")
2823 // `Database::connect_proxy` (database/mod.rs:139, behind the `proxy` feature) is
2824 // `Database::connect`'s sibling for a caller-supplied `ProxyDatabaseTrait` backend —
2825 // same effect (produces a live `DatabaseConnection`), different verb, missing from an
2826 // allowlist keyed on the plain `connect` spelling.
2827 || path.ends_with("::connect_proxy")
2828 || path.ends_with("::execute")
2829 || path.ends_with("::execute_unprepared")
2830 || path.ends_with("::query_one")
2831 || path.ends_with("::query_all")
2832 || path.ends_with("::fetch_page")
2833 || path.ends_with("::num_items")
2834 || r1607_active_model_query(path)
2835 {
2836 return Some("Db");
2837 }
2838 // THE COVERAGE-GATE SWEEP (2026-08-27): sea_orm's TRANSACTION and PAGINATION families,
2839 // verified against sea-orm 1.1.20 source — each of these was found effectful by self-scan
2840 // under no guessed spelling this allowlist already covered.
2841 //
2842 // `DatabaseConnection::transaction`/`transaction_with_config` (db_connection.rs:302,345, the
2843 // `TransactionTrait` impl) is the crate's documented callback-transaction API
2844 // (`db.transaction(|txn| ...)`) — every sqlx-backed match arm dispatches to the
2845 // `SqlxXxxPoolConnection::transaction` methods FQN-listed below, a real BEGIN. FQN-exact
2846 // (not a bare `::transaction` suffix) because `MockDatabaseConnection`/`ProxyDatabaseConnection`
2847 // (driver/mock.rs, driver/proxy.rs, both feature-gated) have their OWN `begin`/`commit`/
2848 // `rollback`/`ping`/`transaction` methods sharing every one of these verb names — a mock
2849 // backend performs no real I/O by construction, and a caller-supplied proxy is a callback
2850 // boundary, not a provable effect; a bare suffix would fabricate Db on both.
2851 if path == "sea_orm::DatabaseConnection::transaction"
2852 || path == "sea_orm::DatabaseConnection::transaction_with_config"
2853 // BONUS, found while proving the above reachable with a real consumer fixture (not in
2854 // the original coverage-gate ratchet — self-scan's own reachability pass didn't flag
2855 // these, apparently because it doesn't track an enum match arm's payload binding as a
2856 // typed receiver, but they dispatch through the identical match-on-`self` shape as
2857 // `transaction` two lines up): `DatabaseConnection::ping` (db_connection.rs:462) and the
2858 // `TransactionTrait::begin`/`begin_with_config` impl (db_connection.rs:246,268) — a fixture
2859 // proved `ping` silently read pure with no rule at all.
2860 || path == "sea_orm::DatabaseConnection::ping"
2861 || path == "sea_orm::DatabaseConnection::begin"
2862 || path == "sea_orm::DatabaseConnection::begin_with_config"
2863 // `DatabaseTransaction::commit`/`rollback` (transaction.rs:119,159) call
2864 // `<sqlx::X as sqlx::Database>::TransactionManager::commit`/`rollback` directly — real
2865 // COMMIT/ROLLBACK, not sea_orm's OWN `::begin`/`::run` (both `pub(crate)`, unreachable).
2866 || path == "sea_orm::DatabaseTransaction::commit"
2867 || path == "sea_orm::DatabaseTransaction::rollback"
2868 // The three sqlx-backed pool connections' OWN `begin`/`ping`/`transaction` (driver/
2869 // sqlx_{mysql,postgres,sqlite}.rs) — `begin`/`transaction` acquire a pool connection and
2870 // start a real transaction, `ping` round-trips `conn.ping()` to the server.
2871 || path == "sea_orm::SqlxMySqlPoolConnection::begin"
2872 || path == "sea_orm::SqlxMySqlPoolConnection::ping"
2873 || path == "sea_orm::SqlxMySqlPoolConnection::transaction"
2874 || path == "sea_orm::SqlxPostgresPoolConnection::begin"
2875 || path == "sea_orm::SqlxPostgresPoolConnection::ping"
2876 || path == "sea_orm::SqlxPostgresPoolConnection::transaction"
2877 || path == "sea_orm::SqlxSqlitePoolConnection::begin"
2878 || path == "sea_orm::SqlxSqlitePoolConnection::ping"
2879 || path == "sea_orm::SqlxSqlitePoolConnection::transaction"
2880 // `Paginator::fetch`/`fetch_and_next`/`into_stream`/`num_pages`/`num_items_and_pages`
2881 // (executor/paginator.rs) all transitively reach `fetch_page`/`num_items` — already-
2882 // covered verbs — but each is ALSO a documented entry point a real consumer calls
2883 // directly (`cake::Entity::find().paginate(db, 50).into_stream()`, the crate's own
2884 // pagination doc example) and needs its own rule for the same reason
2885 // `Connection::backup` needed one beside `Backup::new` in rusqlite.
2886 || path == "sea_orm::Paginator::fetch"
2887 || path == "sea_orm::Paginator::fetch_and_next"
2888 || path == "sea_orm::Paginator::into_stream"
2889 || path == "sea_orm::Paginator::num_pages"
2890 || path == "sea_orm::Paginator::num_items_and_pages"
2891 // `Insert`/`Inserter`/`TryInsert::exec_with_returning_keys`/`_many` (executor/insert.rs)
2892 // are sibling spellings of the already-covered `::exec_with_returning` (same shape as
2893 // rusqlite's `open_with_flags_and_vfs` beside `open_with_flags`): each calls through to
2894 // the same underlying `exec_with_returning_keys`/`_many` executor, a real INSERT.
2895 || path.ends_with("::exec_with_returning_keys")
2896 || path.ends_with("::exec_with_returning_many")
2897 {
2898 return Some("Db");
2899 }
2900 // SOUNDNESS R1528 — sea-orm 2.0.4, the 76 grandfathered rows (above the 1.1.20 ceiling). Every body was READ
2901 // and EXECUTED against a consumer fixture on a file-backed sqlite database (handoff `rust-r1528/seaorm/`).
2902 //
2903 // Terminal verbs on a TYPED receiver. `require_one` is `one(db)` plus a not-found error
2904 // (executor/select.rs:591,681,817,887,1143) and `stream_partial_model` is `into_partial_model().stream(db)`
2905 // (select.rs:620-846, select/{three,four}.rs): the same round trip as the `::one`/`::stream` suffixes above,
2906 // under verbs that list missed. EXECUTED: each returned the row / issued the statement (`sea_orm.query_one`
2907 // span and a `sqlx::query` event). Neither name exists outside the select family, so the suffix is safe.
2908 if path.ends_with("::require_one") || path.ends_with("::stream_partial_model") {
2909 return Some("Db");
2910 }
2911 // The transaction family and the `_raw` statement verbs, FQN-exact for the reason the block above gives
2912 // (the mock and proxy connections share every verb name, and `RestrictedConnection::execute_raw` /
2913 // `query_*_raw` are PURE refusals that return `DbErr::RbacError` without touching the connection,
2914 // restricted_connection.rs:54-81 — a bare `::execute_raw` suffix would fabricate Db on them; EXECUTED, the
2915 // `RestrictedConnection::execute_raw` control returns the RBAC error and issues no statement).
2916 // * `DatabaseConnection`: `begin_with_options` (db_connection.rs:430), `transaction_async` /
2917 // `transaction_with_config_async` (:657/:671, `begin()` + callback + COMMIT/ROLLBACK). EXECUTED: each
2918 // issued `BEGIN`/`COMMIT` (`sea_orm.begin`/`sea_orm.commit` spans, tracing-spans feature) and
2919 // `begin_with_options(Immediate)` held a write lock a second process could not take. The `_raw` verbs
2920 // (`execute_raw`/`query_one_raw`/`query_all_raw`/`stream_raw`, :162-326) are not rows — their self-scan set
2921 // is `Unknown` alone (R1539) — but read pure at a consumer exactly as the rows did, so they carry the same
2922 // rule (EXECUTED: `execute_raw` wrote a row).
2923 // * `DatabaseExecutor` (executor.rs:51-258) and `DatabaseTransaction` (transaction.rs:417-770): every method
2924 // forwards to the connection/transaction it wraps on EVERY arm of its `match`, so none is conditional;
2925 // `DatabaseTransaction::begin*` start a nested SAVEPOINT, `stream_raw` issues the query when polled.
2926 // * `RestrictedConnection` / `RestrictedTransaction` (restricted_connection.rs:150-450): `begin*` and
2927 // `transaction*` call `self.conn.begin*()` before any permission check, `commit`/`rollback` call the
2928 // wrapped transaction's. EXECUTED against an RBAC-enabled sqlite file: BEGIN/COMMIT/ROLLBACK spans, a
2929 // write lock held across `begin_with_options(Immediate)`.
2930 // SOUNDNESS R1528 (sea-orm 2.0.4 refresh, covgate lane): `SqlxPostgresConnector::accepts(url)` is `url.starts_with(
2931 // "postgres://") && url.parse::<PgConnectOptions>().is_ok()` (driver/sqlx_postgres.rs:61), and that parse reads the
2932 // `PG*` environment. EXECUTED (macOS, DYLD interposer, sea-orm 2.0.4): `postgres://u@h/db` = 13 `PG*` getenv + 3
2933 // stat of the unix-socket candidates + open `~/.pgpass` (the second effect: `ALSO_EFFECTS`); `postgres://u:pw@h/db`
2934 // = 12 getenv; a `mysql://` string and a pure control = 0 (the `&&` short-circuits), so the charge is an
2935 // over-approximation for a non-postgres URL, the same stated trade as the `PgConnectOptions::from_str` row (R1623).
2936 if let Some(("SqlxPostgresConnector", "accepts")) =
2937 type_method(path, "sea_orm", &["", "driver", "driver::sqlx_postgres"])
2938 {
2939 return Some("Env");
2940 }
2941 if let Some((ty, m)) = type_method(path, "sea_orm", &[""]) {
2942 if matches!(
2943 (ty, m),
2944 ("DatabaseConnection", "begin_with_options" | "transaction_async" | "transaction_with_config_async"
2945 | "execute_raw" | "query_one_raw" | "query_all_raw" | "stream_raw")
2946 | ("DatabaseExecutor", "begin" | "begin_with_config" | "begin_with_options" | "execute_raw"
2947 | "query_one_raw" | "query_all_raw" | "transaction" | "transaction_async"
2948 | "transaction_with_config" | "transaction_with_config_async")
2949 | ("DatabaseTransaction", "begin" | "begin_with_config" | "begin_with_options" | "execute_raw"
2950 | "query_one_raw" | "query_all_raw" | "stream_raw" | "transaction" | "transaction_async"
2951 | "transaction_with_config" | "transaction_with_config_async")
2952 | ("RestrictedConnection", "begin" | "begin_with_config" | "begin_with_options" | "transaction"
2953 | "transaction_async" | "transaction_with_config" | "transaction_with_config_async")
2954 | ("RestrictedTransaction", "begin" | "begin_with_config" | "begin_with_options" | "commit"
2955 | "rollback" | "transaction" | "transaction_async" | "transaction_with_config"
2956 | "transaction_with_config_async")
2957 ) {
2958 return Some("Db");
2959 }
2960 }
2961 // `rbac` is a `pub mod` and is NOT re-exported at the crate root, so a consumer's path is
2962 // `sea_orm::rbac::RbacContext::m` (the row's root-level spelling is only the generator's guess, and no
2963 // consumer can write it). Each `RbacContext` method opens a transaction on `db` before it
2964 // touches its maps (`db.begin()` / `begin_with_config`, rbac/context.rs:43-300) and `create_tables` runs one
2965 // CREATE TABLE per RBAC table (rbac/schema.rs:11-27). EXECUTED on a fresh sqlite file: `create_tables` made
2966 // the 7 `sea_orm_*` tables, `add_tables`/`add_crud_permissions`/`add_roles`/`add_role_permissions`/
2967 // `remove_role_permissions`/`add_user_override`/`add_role_hierarchy`/`assign_user_role` each changed their
2968 // table's row count (2, 4, 2, 2 then 1, 1, 1, 1) inside a BEGIN/COMMIT, and `load` ran three SELECTs.
2969 // `RbacContext::get_role` (a HashMap lookup) is NOT claimed.
2970 if let Some((ty, m)) = type_method(path, "sea_orm", &["rbac", "rbac::context"]) {
2971 if ty == "RbacContext"
2972 && matches!(m, "load" | "add_tables" | "add_crud_permissions" | "add_roles" | "add_role_permissions"
2973 | "remove_role_permissions" | "add_user_override" | "add_role_hierarchy" | "assign_user_role")
2974 {
2975 return Some("Db");
2976 }
2977 }
2978 if path == "sea_orm::rbac::schema::create_tables" {
2979 return Some("Db");
2980 }
2981 // `SchemaBuilder::apply` (schema/builder.rs:76) runs `CREATE TABLE` for every registered entity and `sync`
2982 // (:88, needs `schema-sync`) discovers the live schema with a dozen `query_all`s and then migrates it.
2983 // EXECUTED: `apply` created table `post`; `sync` issued 40+ `sea_orm.query_all` statements. `register` /
2984 // `Schema::new` only build values and are NOT claimed (control fns stay 0).
2985 if let Some((ty, m)) = type_method(path, "sea_orm", &["", "schema", "schema::builder"]) {
2986 if ty == "SchemaBuilder" && matches!(m, "apply" | "sync") {
2987 return Some("Db");
2988 }
2989 }
2990 // `get_schema_registry` and `EntityRegistry::build_schema` (entity/registry.rs) iterate the `inventory` of
2991 // derive-registered entities and emit one `debug!` per entity (`Registering entities with prefix`,
2992 // `Registered <path>`); the registered `schema_info` callbacks are macro-generated value constructors. No
2993 // statement is issued. EXECUTED: a tracing layer saw one event per registered entity from target
2994 // `sea_orm::entity::registry` (36 for the `tests_cfg` prefix), no `sea_orm.*` span. `Log` is the one effect.
2995 if let Some((ty, m)) = type_method(path, "sea_orm", &["", "entity", "entity::registry"]) {
2996 if (ty, m) == ("EntityRegistry", "build_schema") {
2997 return Some("Log");
2998 }
2999 }
3000 if let Some((ty, m)) = type_method(path, "sea_orm", &[""]) {
3001 if matches!((ty, m), ("DatabaseConnection" | "DatabaseExecutor", "get_schema_registry")) {
3002 return Some("Log");
3003 }
3004 }
3005 return None;
3006 }
3007 // (Reached by sqlx + diesel — the build-vs-execute-split crates.) `first` is diesel's
3008 // LIMIT-1 round trip and `load_iter` its 2.x streaming execution; `fetch_many` is sqlx's
3009 // multi-result stream. All crate-gated, so a std `Vec::first` never resolves here.
3010 //
3011 // `establish` is diesel's OWN name for `::connect` — `Connection::establish(url)`
3012 // (connection/mod.rs:243), implemented by opening the real backend handle in
3013 // `SqliteConnection`/`PgConnection`/`MysqlConnection::establish` (sqlite/connection/mod.rs:230,
3014 // pg/connection/mod.rs:176, mysql/connection/mod.rs:158 — each does the real file-open/socket-
3015 // connect). It is diesel's canonical, and by far most common, connection entry point — every
3016 // diesel quickstart opens with it — yet it shares no verb spelling with `::connect`, so it fell
3017 // through this allowlist to `None`, and because `diesel` IS a CALIBRATED_CRATES entry the miss
3018 // produced NO `coverage.uncovered` disclosure either (that ledger is crate-level: a calibrated
3019 // crate's unmatched path reads as reviewed-pure, not as a gap) — the same silent-purity shape as
3020 // `ignore::Walk::new` above, one call away from `establish_test_transaction`/`establish_inner`,
3021 // which are diesel's own private plumbing and stay unmatched (no `::` in the allowed suffix keeps
3022 // this exact-verb, not a substring match).
3023 // SOUNDNESS R1528 (b10) — diesel 2.3.14 (above its 2.3.13 ceiling). See `diesel_r1528`.
3024 if crate_name == "diesel" {
3025 if let Some(effect) = diesel_r1528(path) {
3026 return Some(effect);
3027 }
3028 }
3029 const VERBS: [&str; 20] = [
3030 "::execute", "::query_row", "::query_map", "::query_one", "::fetch_one",
3031 "::fetch_all", "::fetch_optional", "::fetch", "::fetch_many", "::connect",
3032 "::acquire", "::begin", "::commit", "::rollback", "::load", "::load_iter",
3033 "::first", "::get_result", "::get_results", "::establish",
3034 ];
3035 if VERBS.iter().any(|v| path.ends_with(v)) {
3036 return Some("Db");
3037 }
3038 return None;
3039 }
3040 // std::path::Path / PathBuf STAT-family methods hit the filesystem (each is a stat/readlink/
3041 // readdir syscall) — unlike the rest of the std::path surface, which is pure string manipulation
3042 // (join/file_name/extension/parent/…). Verb-precise so the scanner's receiver inference can safely
3043 // route a `path.symlink_metadata()` method call here. (A blackout screen caught gix-dir — an entire
3044 // directory WALKER — reporting ZERO Fs because all its I/O is Path-method calls; same class as
3045 // fd's residual `Path::symlink_metadata` under-report.)
3046 if let Some(m) = path
3047 .strip_prefix("std::path::Path::")
3048 .or_else(|| path.strip_prefix("std::path::PathBuf::"))
3049 {
3050 const STAT: &[&str] = &[
3051 "metadata", "symlink_metadata", "canonicalize", "read_link", "read_dir", "exists",
3052 "try_exists", "is_file", "is_dir", "is_symlink",
3053 ];
3054 return STAT.contains(&m).then_some("Fs");
3055 }
3056 // OPTION-BUILDERS, the shape SPEC §1 ⟨0.32⟩ separates from an invocation object: "option-builders for
3057 // other effects (`OpenOptions`, request builders) stay pure because their resource arrives at the
3058 // terminal verb, which is charged at its own call site". An `OpenOptions` holds a handful of bools and
3059 // names no file; `DirBuilder` holds one bool and a mode. Both sit under the coarse `std::fs::` prefix
3060 // below, which charged Fs for `OpenOptions::new()` and for every `o.read(true)` — MEASURED: a
3061 // `let o = OpenOptions::new().read(true);` with no `open` ANYWHERE reported `Fs`.
3062 //
3063 // A DENYLIST, and keyed on the TYPE. Only the provably-pure setters/ctors are subtracted; anything
3064 // else under the type keeps its effect, so the terminal verb (`OpenOptions::open`, `DirBuilder::create`)
3065 // stays Fs and a std addition we have not read about fails in the safe direction. The type key is not
3066 // decoration: `create` is a pure flag SETTER on `OpenOptions` and the mkdir SYSCALL on `DirBuilder` —
3067 // one leaf, opposite answers. (candor-java keys the same carve-out by DESCRIPTOR, because its collision
3068 // is an overload — `command()` reads back, `command(List)` sets. Rust has no overloads, so the type IS
3069 // the discriminator; a bare-name denylist would silence `DirBuilder::create`.)
3070 if let Some(m) = path.strip_prefix("std::fs::OpenOptions::") {
3071 const PURE: &[&str] = &[
3072 // the builder itself + the std flag setters
3073 "new", "read", "write", "append", "truncate", "create", "create_new",
3074 // the platform extension setters (`OpenOptionsExt`, unix + windows)
3075 "mode", "custom_flags", "security_qos_flags", "access_mode", "share_mode", "attributes",
3076 // derives
3077 "clone", "default", "fmt", "eq", "ne", "hash",
3078 ];
3079 return (!PURE.contains(&m)).then_some("Fs");
3080 }
3081 if let Some(m) = path.strip_prefix("std::fs::DirBuilder::") {
3082 const PURE: &[&str] = &["new", "recursive", "mode", "clone", "default", "fmt"];
3083 return (!PURE.contains(&m)).then_some("Fs");
3084 }
3085 // THE PLATFORM `fs` MODULES — `std::os::{unix,windows,wasi}::fs`. Measured 2026-09-02, ground truth
3086 // EXECUTED (a `cargo run` that really created a symlink on disk and then stat'd it back):
3087 // `std::os::unix::fs::symlink`, `chown`, `lchown` and `chroot` all read PURE, so `deny Fs` over a
3088 // crate whose only filesystem write is a symlink exited 0. The `std::fs::` prefix below is the whole
3089 // filesystem rule and these modules are simply not under it — the platform-specific half of std's
3090 // filesystem API had no rule at all. (Found tracing why `tokio::fs::symlink` carries no `Fs` of its
3091 // own; its body is `asyncify(move || std::os::unix::fs::symlink(..))`.)
3092 //
3093 // A DENYLIST keyed on the TRAIT, exactly like `OpenOptions`/`DirBuilder` above and for the same
3094 // reason: subtract only the provably-pure surfaces, so a std addition nobody here has read about
3095 // fails in the SAFE direction. What is subtracted, and why each is pure:
3096 // * `MetadataExt` / `DirEntryExt` / `FileTypeExt` — accessors over data ALREADY fetched by the
3097 // `metadata()`/`read_dir()` call that is charged at its own site. `m.uid()` issues no syscall.
3098 // * `PermissionsExt` — reads/sets bits on an in-memory `Permissions`; the syscall is
3099 // `set_permissions`, charged under `std::fs::`.
3100 // * `OpenOptionsExt` / `DirBuilderExt` — SPEC §1 ⟨0.32⟩ option-builders: the resource arrives at
3101 // the terminal verb (`open`/`create`), which is charged at its own call site.
3102 // `FileExt` is deliberately NOT subtracted — `read_at`/`write_at`/`seek_read`/`seek_write` are the
3103 // positional-I/O syscalls and are exactly what this rule must keep.
3104 if let Some(rest) = path
3105 .strip_prefix("std::os::unix::fs::")
3106 .or_else(|| path.strip_prefix("std::os::windows::fs::"))
3107 .or_else(|| path.strip_prefix("std::os::wasi::fs::"))
3108 {
3109 const PURE_TRAITS: &[&str] = &[
3110 "MetadataExt::", "DirEntryExt::", "DirEntryExt2::", "FileTypeExt::",
3111 "PermissionsExt::", "OpenOptionsExt::", "DirBuilderExt::",
3112 ];
3113 return (!PURE_TRAITS.iter().any(|t| rest.starts_with(t))).then_some("Fs");
3114 }
3115 // THE PLATFORM `process` MODULES — `std::os::{unix,windows,wasi}::process`. SOUNDNESS R141, the
3116 // EXACT SIBLING of the `fs` rule above, and unruled for the same reason it was: R130 closed
3117 // `std::os::*::fs` and translated that question across ENGINES while never translating it across
3118 // SIBLING MODULES of the namespace it had just found unruled.
3119 //
3120 // Ground truth EXECUTED when the row was filed: a consumer printed `before`, then
3121 // `EXECED-THROUGH-LIB`, and never reached the following line — `CommandExt::exec` is `execvp` and
3122 // really does replace the process image. Re-measured at HEAD 2026-09-27 on a 5-function fixture:
3123 // `CommandExt::exec(c)`, `std::os::unix::process::CommandExt::exec(c)` and
3124 // `fn f<C: CommandExt>(c: &mut C) { c.exec() }` were all ABSENT from `functions[]` — no `Unknown`,
3125 // no `unresolved` — and `deny Exec`, `pure` and `deny Exec Unknown` all exited 0, while on the SAME
3126 // tree the RECEIVER spelling `c.exec()` exited 1 and the sibling-namespace UFCS control
3127 // `FileExt::read_at(f, b, 0)` exited 1. So this is the one-spelling-of-two shape of R130 itself:
3128 // `c.exec()` was only ever charged because the receiver types to `std::process::Command` and the
3129 // whole-type rule below answers; nothing answered for the trait-path spelling.
3130 //
3131 // A DENYLIST keyed on the TRAIT, exactly like the `fs` rule and for the same reason — subtract only
3132 // the provably-pure surfaces so a std addition nobody here has read about fails in the SAFE
3133 // direction. What is subtracted, and why each is pure:
3134 // * `ExitStatusExt` / `ExitStatusError` / `ExitCodeExt` — accessors and constructors over an
3135 // ALREADY-COLLECTED status (`signal()`, `core_dumped()`, `from_raw(n)`). The wait that produced
3136 // it is charged at its own site under `std::process::Child`.
3137 // * `parent_id` — `getppid()`, a read of this process's own identity, no spawn. Kept at PARITY
3138 // with `std::process::id`, which carries no rule either; subtracting it here rather than
3139 // leaving it to the Exec cliff is the one place this rule departs from "unrecognised keeps
3140 // Exec", and it is named rather than folded in.
3141 // Everything else keeps Exec: `CommandExt::{exec, pre_exec, arg0, uid, gid, groups,
3142 // process_group}` and the windows `CommandExt::{creation_flags, raw_arg, …}` / `ChildExt`. That is
3143 // also what the receiver spelling already answers — `c.uid(0)` reports `['Exec']` at HEAD — so this
3144 // rule makes the two spellings agree rather than inventing a new verdict for either.
3145 if let Some(rest) = path
3146 .strip_prefix("std::os::unix::process::")
3147 .or_else(|| path.strip_prefix("std::os::windows::process::"))
3148 .or_else(|| path.strip_prefix("std::os::wasi::process::"))
3149 {
3150 const PURE: &[&str] =
3151 &["ExitStatusExt::", "ExitStatusError::", "ExitCodeExt::", "parent_id"];
3152 return (!PURE.iter().any(|t| rest.starts_with(t))).then_some("Exec");
3153 }
3154 // Filesystem. `tokio::fs`/`async_std::fs` are the async mirrors of `std::fs`; `async_fs` is
3155 // smol's fs crate; `fs_err` is a drop-in `std::fs` wrapper (its whole surface is fs I/O).
3156 if path.starts_with("std::fs::")
3157 || path.starts_with("tokio::fs::")
3158 || path.starts_with("async_std::fs::")
3159 || crate_name == "async_fs"
3160 || crate_name == "fs_err"
3161 {
3162 return Some("Fs");
3163 }
3164 // memmap2: only `MmapOptions::map*` (and the in-place `Mmap::flush`/`make_*` protection
3165 // changes / `remap`) actually issue the mmap/msync/mprotect/mremap syscall = Fs. The rest of the
3166 // crate is PURE: `MmapOptions::new`/setters BUILD the request, and once a region is mapped, reads
3167 // over it (`Mmap::len`/`is_empty`/`as_ptr`/`as_mut_ptr`/`deref` into the byte slice) are plain
3168 // memory access with no syscall. Whole-crate Fs fabricated Fs on those reads (a `m.len()` the
3169 // scanner's receiver inference routes to `memmap2::Mmap::len`). Match the syscall-issuing verbs;
3170 // everything else returns None (pure). `map*` covers `map`/`map_mut`/`map_exec`/`map_copy`/
3171 // `map_copy_read_only`/`map_raw`/`map_raw_read_only`/`map_anon`.
3172 if crate_name == "memmap2" {
3173 let m = path.rsplit("::").next().unwrap_or(path);
3174 if m.starts_with("map")
3175 || m == "flush"
3176 || m == "flush_async"
3177 || m == "flush_range"
3178 || m == "flush_async_range"
3179 || m == "remap"
3180 || m.starts_with("make_")
3181 || m == "advise"
3182 || m == "advise_range"
3183 || m == "lock"
3184 || m == "unlock"
3185 {
3186 return Some("Fs");
3187 }
3188 return None;
3189 }
3190 // tempfile: creating a temp file/dir touches the disk. Match the create/persist verbs (the
3191 // `Builder` setters — prefix/suffix/rand_bytes — stay pure). `persist`/`keep` rename/retain
3192 // the file on disk; `close` removes it.
3193 // SOUNDNESS R1528 (b10) — `TempDir::keep` is carved OUT of the `::keep` suffix below. See `tempfile_r1528`.
3194 if crate_name == "tempfile" {
3195 if matches!(type_method(path, "tempfile", &["", "dir"]), Some(("TempDir", "keep"))) {
3196 return None;
3197 }
3198 if let Some(effect) = tempfile_r1528(path) {
3199 return Some(effect);
3200 }
3201 }
3202 if crate_name == "tempfile"
3203 && (path.ends_with("::tempfile")
3204 || path.ends_with("::tempfile_in")
3205 || path.ends_with("::tempdir")
3206 || path.ends_with("::tempdir_in")
3207 || path.ends_with("NamedTempFile::new")
3208 || path.ends_with("NamedTempFile::new_in")
3209 || path.ends_with("TempDir::new")
3210 || path.ends_with("TempDir::new_in")
3211 || path.ends_with("::persist")
3212 || path.ends_with("::persist_noclobber")
3213 || path.ends_with("::keep"))
3214 {
3215 return Some("Fs");
3216 }
3217 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against tempfile 3.27.0 source — the SIBLING
3218 // constructors/closers the create/persist verb list above never reached:
3219 //
3220 // `Builder::make`/`make_in` (lib.rs:716,739) are the crate's OWN documented escape hatch for a
3221 // caller-supplied factory (`Builder::new().make_in(dir, |path| UnixListener::bind(path))`) — real
3222 // disk creation via `util::create_helper`, same mechanism as `Builder::tempfile`/`tempfile_in`
3223 // two lines up, just a different entry verb.
3224 // `NamedTempFile::with_prefix`/`with_prefix_in`/`with_suffix`/`with_suffix_in` (file/mod.rs:630-677)
3225 // are one-call convenience wrappers documented as equivalent to `Builder::new().prefix(..).tempfile()`
3226 // — real creation, missing because the verb list matched `NamedTempFile::new`/`new_in` but not these.
3227 // `NamedTempFile::reopen` (file/mod.rs:951) calls the platform `imp::reopen`, a real `open`/`fstat`
3228 // pair (found the original file was replaced, or hands back a fresh handle to it).
3229 // `TempPath::close` (file/mod.rs:161) and `TempDir::close` (dir/mod.rs:470) `fs::remove_file`/
3230 // `remove_dir_all` the real path — `NamedTempFile::close` (file/mod.rs:727) is a thin `self.path.close()`
3231 // delegate to the same `TempPath::close`, so matching the `::close` suffix (crate-gated, and the ONLY
3232 // three `pub fn close` in this crate) catches it too without a separate rule.
3233 // `SpooledTempFile::set_len` (spooled.rs:134) calls `File::set_len` on the real disk file once the
3234 // in-memory buffer has spilled past `max_size` — the crate's own roll-over threshold, not a corner case.
3235 //
3236 // NOT included: `tempfile::create`/`create_named`/`reopen` (dir/imp/any.rs, file/imp/windows.rs,
3237 // file/imp/unix.rs) — despite being `pub fn`, `mod dir;`/`mod file;` are PRIVATE at the crate root
3238 // (lib.rs:202,204) with no re-export of the `imp` submodules, so no external consumer can ever name
3239 // these paths; the ratchet's generator guesses a crate-root alias for every candidate regardless of
3240 // whether one is real (the same "second, related shape" gap the previous pass's rusqlite/lettre
3241 // private-module findings already documented — restricted_types()/is_bare_pub check a type's or fn's
3242 // OWN visibility keyword, not its enclosing module chain). Removed from open.tsv rather than guessed.
3243 if crate_name == "tempfile"
3244 && (path == "tempfile::Builder::make"
3245 || path == "tempfile::Builder::make_in"
3246 || path == "tempfile::NamedTempFile::with_prefix"
3247 || path == "tempfile::NamedTempFile::with_prefix_in"
3248 || path == "tempfile::NamedTempFile::with_suffix"
3249 || path == "tempfile::NamedTempFile::with_suffix_in"
3250 || path == "tempfile::NamedTempFile::reopen"
3251 || path == "tempfile::SpooledTempFile::set_len"
3252 || path.ends_with("::close"))
3253 {
3254 return Some("Fs");
3255 }
3256 // SOUNDNESS R1528 — two more families the verb lists above did not reach, both EXECUTED 2026-11-08
3257 // against tempfile 3.27.0 and read in source:
3258 //
3259 // `NamedTempFile::into_file` (file/mod.rs:894) moves the `File` out and DROPS the `TempPath`, whose
3260 // `Drop` unlinks the path (its doc: "the underlying file will be removed from the filesystem"). The run
3261 // printed `exists-before=true`, `exists-after-into_file=false`. A filesystem mutation with no `fs::`
3262 // in sight, which is why it read pure. (`SpooledTempFile::into_file` is a different body and is not
3263 // claimed; `TempDir::into_path` only flips a flag — verified FALSE by the independent reader.)
3264 //
3265 // `TempDir::with_prefix`/`with_prefix_in`/`with_suffix`/`with_suffix_in` (dir/mod.rs:269-350) are
3266 // one-line wrappers over `Builder::tempdir`/`tempdir_in`, the same directory creation `TempDir::new`
3267 // is already charged for. Fs (the creation), the bucket `TempDir::new` carries — the oracle's
3268 // Env/Rand are the name generation and are not what the call is for.
3269 if crate_name == "tempfile"
3270 && (path == "tempfile::NamedTempFile::into_file"
3271 || path == "tempfile::TempDir::with_prefix"
3272 || path == "tempfile::TempDir::with_prefix_in"
3273 || path == "tempfile::TempDir::with_suffix"
3274 || path == "tempfile::TempDir::with_suffix_in")
3275 {
3276 return Some("Fs");
3277 }
3278 // glob: walks the filesystem to expand a pattern (the returned iterator reads directories).
3279 // `Pattern::matches` is pure string matching — match only the directory-walking entry points.
3280 if crate_name == "glob" && (path.ends_with("::glob") || path.ends_with("::glob_with")) {
3281 return Some("Fs");
3282 }
3283 // Password-hashing / KDF crates — the entropy tier (the TS engine's CTA lesson: an invisible
3284 // argon2 landed on exactly the call a security review cares about). In this engine's
3285 // verb-precise style the ENTROPY is the salt mint: `SaltString::generate(OsRng)` in the
3286 // password-hash API family, and bcrypt's `hash`/`hash_with_result` (salt minted internally).
3287 // Verification and explicit-salt hashing are deterministic recomputation — pure. `rand_core`
3288 // carries the OsRng source itself (otherwise the most common salt mint is invisible).
3289 if matches!(crate_name, "argon2" | "scrypt" | "pbkdf2" | "password_hash") {
3290 // SOUNDNESS R318 — `SaltString::generate` was this block's ONLY match target and the type was
3291 // DELETED upstream in password-hash 0.6.0 (0.5.0: 21 occurrences; 0.6.1: zero). Everything below
3292 // it fell through to `return None`, so all four crates asserted purity — including on
3293 // `argon2.hash_password(password)`, the one-argument DEFAULT TRAIT METHOD at
3294 // `password-hash-0.6.1/src/lib.rs:107` whose body is `let salt = try_generate_salt()?;`. That is
3295 // the most documented call shape in the family, minting OS entropy, reading pure, on crates whose
3296 // entire purpose is a security review.
3297 //
3298 // `ends_with`, NOT `contains`: `hash_password_with_salt` takes an explicit salt and is
3299 // deterministic recomputation — correctly PURE — and a `contains` match would fabricate `Rand`
3300 // on it. `hash_password_with_rng` DOES draw, from the caller's RNG, so it is charged.
3301 // The old `SaltString::generate` clause is kept: it is still the right answer for 0.5.x.
3302 if path.contains("SaltString::generate")
3303 || path.ends_with("::generate_salt")
3304 || path.ends_with("::try_generate_salt")
3305 // SOUNDNESS R330 — this suffix is a DEFAULT, refined by arity at the call site. `hash_password`
3306 // is one name with two signatures (see `password_hash_explicit_salt` below), and the path
3307 // cannot tell them apart. Charging here and NARROWING there is deliberate: a consumer that
3308 // never consults the refinement keeps the over-report, never the silence.
3309 || path.ends_with("::hash_password")
3310 || path.ends_with("::hash_password_with_rng")
3311 {
3312 return Some("Rand");
3313 }
3314 return None;
3315 }
3316 if crate_name == "bcrypt" {
3317 // SOUNDNESS R319 — this list was EXACTLY bcrypt's internal-salt surface at 0.15.1 and fell
3318 // behind by ADDITION, not by rename: 0.19.3 adds four more verbs that each document "the salt
3319 // is generated randomly using the OS randomness" and reach `getrandom::fill`. A stale-PATH
3320 // check could never have found this, because every old path still exists and still works —
3321 // a version ceiling catches additions, a path check only catches removals.
3322 // The explicit-salt family (`hash_with_salt`, `hash_with_salt_bytes`,
3323 // `non_truncating_hash_with_salt*`) stays PURE: the caller supplies the salt.
3324 if path.ends_with("::hash")
3325 || path.ends_with("::hash_with_result")
3326 || path.ends_with("::non_truncating_hash")
3327 || path.ends_with("::hash_bytes")
3328 || path.ends_with("::non_truncating_hash_bytes")
3329 || path.ends_with("::non_truncating_hash_with_result")
3330 {
3331 return Some("Rand");
3332 }
3333 return None;
3334 }
3335 if crate_name == "rand_core" {
3336 if path.contains("OsRng")
3337 || path.ends_with("::next_u32")
3338 || path.ends_with("::next_u64")
3339 || path.ends_with("::fill_bytes")
3340 // SOUNDNESS R333 — the FALLIBLE trait. rand_core 0.10 splits `RngCore` from `TryRngCore`,
3341 // and `SysRng` (the renamed `OsRng`) is fallible, so `try_next_u32` is the natural API on
3342 // it. `ends_with("::next_u32")` does NOT match `::try_next_u32`, so the modern spelling of
3343 // a real entropy draw read PURE. Third instance in one day, after R319 and R321, of the
3344 // same shape: an upstream rename is rarely just a rename — it arrives with an API SHAPE
3345 // change, and a suffix list written against the old shape stops covering the new one.
3346 || path.ends_with("::try_next_u32")
3347 || path.ends_with("::try_next_u64")
3348 || path.ends_with("::try_fill_bytes")
3349 {
3350 return Some("Rand");
3351 }
3352 return None;
3353 }
3354 // Randomness / entropy. `getrandom`/`fastrand` are effectful end-to-end. `rand` is NOT — it
3355 // mixes entropy/generation (effectful) with *pure* distribution constructors (`Uniform::new`,
3356 // `Normal::new`) and deterministic-seed constructors (`seed_from_u64`). Flagging the whole crate
3357 // over-reported those as `Rand`; match only the calls that actually consume randomness — the
3358 // entropy sources (`OsRng`, `thread_rng`/`rng`, `from_entropy`/`from_os_rng`) and the generation
3359 // verbs (`gen*`/`random*`/`fill*`/`sample*`/`next_u*`). A `Uniform::new` is now correctly pure.
3360 if crate_name == "getrandom" {
3361 // SOUNDNESS R1150 — …except its ERROR VALUE. `getrandom::Error` is a wrapped error code; its own methods
3362 // (`raw_os_error`, `code`, `new_custom`, the derived `eq`/`hash`, `Debug`/`Display`) read or format that
3363 // code and draw nothing. Charged with the crate, rand_core 0.9.x's DERIVED `OsError::eq` read `Rand`
3364 // (EXECUTED: no draw happens). Every drawing entry point (`fill`, `fill_uninit`, `u32`, `u64`,
3365 // `getrandom`, `SysRng`'s methods) is not on `Error`, and keeps `Rand`.
3366 // `&Error` is the by-reference operator key (`getrandom::&Error::eq`, R1131).
3367 if path.split("::").skip(1).any(|s| s.trim_start_matches('&') == "Error") {
3368 return None;
3369 }
3370 return Some("Rand");
3371 }
3372 // fastrand: like `rand`, it mixes entropy-consuming generation (effectful) with PURE deterministic
3373 // pieces. `Rng::with_seed(42)` is a DETERMINISTIC seeded constructor (consumes no entropy — the same
3374 // seed gives the same stream), and `Rng::fork`/`Rng::clone` just split/copy existing state. Those are
3375 // PURE; whole-crate Rand fabricated Rand on them. The effect is the value-drawing methods (`u32`/
3376 // `usize`/`bool`/`f64`/`char`/`alphanumeric`/`choice`/`choose_multiple`/`shuffle`/`fill`/the range
3377 // forms) AND the entropy-seeded entry points: bare `Rng::new()` (seeds from the global entropy-backed
3378 // generator), `fastrand::seed`, and the top-level `fastrand::u32(..)` free functions (which draw from
3379 // the thread-local generator). `with_seed` is exempted explicitly; any other method on an `Rng`
3380 // (i.e. a value draw) is Rand.
3381 if crate_name == "fastrand" {
3382 let m = path.rsplit("::").next().unwrap_or(path);
3383 // Provably pure: deterministic seeded ctor + state split/copy.
3384 if m == "with_seed" || m == "fork" || m == "clone" {
3385 return None;
3386 }
3387 // Everything else fastrand exposes either draws a value or seeds from entropy → Rand. (The crate
3388 // has no pure data types beyond the `Rng` handle itself, so a non-draw stray would have to be a
3389 // method we don't recognise — keep the effect, the safe direction.)
3390 return Some("Rand");
3391 }
3392 if crate_name == "rand" {
3393 let rng_verb = path.ends_with("::gen")
3394 || path.ends_with("::gen_range")
3395 || path.ends_with("::gen_bool")
3396 || path.ends_with("::gen_ratio")
3397 || path.ends_with("::random")
3398 || path.ends_with("::random_range")
3399 || path.ends_with("::random_bool")
3400 || path.ends_with("::random_ratio")
3401 || path.ends_with("::random_iter") // rand 0.9 iterator generator
3402 || path.ends_with("::gen_iter")
3403 || path.ends_with("::fill")
3404 || path.ends_with("::fill_bytes")
3405 || path.ends_with("::try_fill")
3406 || path.ends_with("::try_fill_bytes")
3407 || path.ends_with("::sample")
3408 || path.ends_with("::sample_iter")
3409 || path.ends_with("::next_u32")
3410 || path.ends_with("::next_u64")
3411 // SOUNDNESS R333 — the FALLIBLE trait, in the `rand` block as well as `rand_core`'s. Two
3412 // verb lists answer one question ("is this a draw?") and only one had been widened, which
3413 // is why the first half of this fix moved nothing: correct code on a path the call does not
3414 // take. `TryRngCore`'s verbs are the natural API on the fallible `SysRng`.
3415 || path.ends_with("::try_next_u32")
3416 || path.ends_with("::try_next_u64")
3417 || path.ends_with("::thread_rng")
3418 || path.ends_with("::rng")
3419 || path.ends_with("::from_entropy")
3420 || path.ends_with("::from_os_rng");
3421 // `OsRng` is the OS entropy SOURCE, but `clone`/`fork`/`default` just copy or construct the
3422 // (zero-sized) handle and draw no entropy — pure, exactly like the `fastrand` arm's clone/fork
3423 // exemption above. The actual draws (`fill_bytes`/`next_u*`/…) are caught by `rng_verb`. Without
3424 // this exemption the blanket `contains("OsRng")` fabricated `Rand` on `OsRng::clone` (adversarial
3425 // review: OsRng is a unit struct, cloning consumes nothing).
3426 let m = path.rsplit("::").next().unwrap_or(path);
3427 let os_rng = path.contains("OsRng") && !matches!(m, "clone" | "fork" | "default");
3428 // SOUNDNESS R320 — …and the SAME clause for the type's CURRENT NAME. rand 0.10 renamed the OS
3429 // entropy source to `SysRng` and moved it to `getrandom`, re-exporting it as
3430 // `rand::rngs::SysRng` (`rand-0.10.2/src/rngs/mod.rs:119`). Measured, not assumed: candor
3431 // resolves the crate from the path the CONSUMER writes, so `rand::rngs::SysRng` arrives with
3432 // crate_name `rand` and lands here — NOT in getrandom's whole-crate exemption, which is what
3433 // R320 left open. The asymmetry that leaves was executed on a fixture: with `OsRng` as the
3434 // receiver an unrecognised verb charges `Rand` (the `contains` clause is the safety margin over
3435 // the verb list); with `SysRng` it read PURE. So the margin protected the DELETED spelling and
3436 // not its replacement, which is the wrong way round for a type whose only purpose is to draw
3437 // from the OS. Same construction exemption for the same reason — `SysRng` is a unit struct
3438 // (`getrandom-0.4.3/src/sys_rng.rs:34`), so copying the handle consumes nothing.
3439 let sys_rng = path.contains("SysRng") && !matches!(m, "clone" | "fork" | "default");
3440 // §E1 HIT COUNTER (`CANDOR_ALIAS_DEBUG`), for `bin/corpus-ab.py --mark`: a widened charge that
3441 // moves no rows is either inert or unreached, and those two print identically.
3442 if sys_rng && candor_report::env::trace("CANDOR_ALIAS_DEBUG") {
3443 eprintln!("R320SYSRNG {path}");
3444 }
3445 if rng_verb || os_rng || sys_rng {
3446 return Some("Rand");
3447 }
3448 return None;
3449 }
3450 // Subprocess spawning. `tokio::process` is the async mirror of `std::process` — it exists
3451 // only to spawn/control subprocesses (`Command`/`Child`, no pure data types like std's
3452 // `Stdio`/`ExitStatus`/`exit`), so spawning through it is Exec just the same. Without this an
3453 // async app's `tokio::process::Command::new(..).spawn()` classified pure — a silent under-report
3454 // of subprocess execution, the dangerous direction (mirrors the tokio::fs/tokio::net coverage).
3455 if path.starts_with("std::process::Command")
3456 || path.starts_with("std::process::Child")
3457 || path.starts_with("tokio::process::Command")
3458 || path.starts_with("tokio::process::Child")
3459 || path.starts_with("async_std::process::Command")
3460 || path.starts_with("async_std::process::Child")
3461 {
3462 // PURE read-backs of the builder's stored fields / the cached pid — no spawn, no syscall — so the
3463 // whole-type Exec rule fabricated Exec on them (sweep [23]; mirrors the portable_pty getter carve-
3464 // out just below). get_program/get_args/get_envs/get_current_dir read the Command; Child::id reads
3465 // the cached pid. Every genuine verb (new/spawn/output/status/wait/kill) stays Exec.
3466 if path.ends_with("::get_program")
3467 || path.ends_with("::get_args")
3468 || path.ends_with("::get_envs")
3469 || path.ends_with("::get_current_dir")
3470 || path.ends_with("Child::id")
3471 {
3472 return None;
3473 }
3474 return Some("Exec");
3475 }
3476 // portable_pty / async_process are whole-crate Exec EXCEPT for the proven-pure surface they expose:
3477 // the `CommandBuilder` GETTERS (`get_argv`/`get_cwd`/`get_env`/`as_unix_command_line`…) read back
3478 // configuration, and the PURE DATA types (`PtySize::default`, `ExitStatus`/`Stdio`/`CommandBuilder`
3479 // construction/setters). The earlier `is_cmd_naming_method` fix stopped the head-refinement LEAK, but
3480 // the BASE Exec still fabricated on these accessors (a `cmd.get_cwd()` the scanner routes to
3481 // `portable_pty::CommandBuilder::get_cwd`). Subtract the read-back getters and the obvious pure
3482 // ctors/setters; the spawn/wait/exec surface (`spawn_command`/`openpty`/`wait`/`kill`/`exec`…) keeps
3483 // Exec. SUBTRACT only what is provably pure — when unrecognised, KEEP Exec (the safe direction).
3484 if crate_name == "async_process" || crate_name == "portable_pty" {
3485 let m = path.rsplit("::").next().unwrap_or(path);
3486 // THE COVERAGE-GATE SWEEP (2026-08-27): `CommandBuilder::get_shell` (cmdbuilder.rs:544, unix)
3487 // LOOKS like a config read-back getter — same `get_` naming as the pure getters below — but its
3488 // unix body calls `nix::unistd::access(shell, X_OK)`, a real filesystem-access syscall, before
3489 // falling back to a password-database lookup if `$SHELL` isn't set or isn't executable. Carved
3490 // out of the `get_` prefix exemption BEFORE it, not folded into it — the one getter this crate
3491 // exposes that is not actually pure. Verified against portable-pty 0.9.0.
3492 if m == "get_shell" {
3493 return Some("Fs");
3494 }
3495 // configuration read-back getters — pure (no spawn).
3496 if m.starts_with("get_") || m == "as_unix_command_line" {
3497 return None;
3498 }
3499 // pure data-type ctors/setters/derives that NAME no program and spawn nothing.
3500 if matches!(
3501 m,
3502 "default" | "new" | "piped" | "null" | "inherit" | "from_raw_fd"
3503 | "arg" | "args" | "arg0" | "env" | "envs" | "env_clear" | "env_remove"
3504 | "cwd" | "current_dir" | "rows" | "cols"
3505 | "clone" | "fmt" | "eq" | "ne" | "hash"
3506 ) {
3507 return None;
3508 }
3509 return Some("Exec");
3510 }
3511 // duct: a subprocess-orchestration crate. `cmd()`/`cmd!` only *build* an Expression; the
3512 // spawn/wait happens at `run`/`read`/`start`. Match the execution verbs, not the builder.
3513 if crate_name == "duct"
3514 && (path.ends_with("::run")
3515 || path.ends_with("::read")
3516 || path.ends_with("::start")
3517 || path.ends_with("::read_chars")
3518 // `Expression::reader` (lib.rs:357) calls the already-covered `.start()` internally to
3519 // launch the child and hands back a streaming `ReaderHandle` — a distinct public entry
3520 // point from `start` itself, so the suffix match above never reached it.
3521 || path.ends_with("Expression::reader"))
3522 {
3523 return Some("Exec");
3524 }
3525 if path.starts_with("std::env::") {
3526 return Some("Env");
3527 }
3528 // dotenvy / dotenv: load environment variables (reading a `.env` file and mutating the process
3529 // environment). Match the load/read entry points; `Error`/builder types stay pure.
3530 if matches!(crate_name, "dotenvy" | "dotenv")
3531 && (path.ends_with("::dotenv")
3532 || path.ends_with("::dotenv_override")
3533 || path.ends_with("::from_path")
3534 || path.ends_with("::from_path_override")
3535 || path.ends_with("::from_filename")
3536 || path.ends_with("::from_filename_override")
3537 || path.ends_with("::from_read")
3538 || path.ends_with("::from_read_override")
3539 || path.ends_with("::load")
3540 || path.ends_with("::var")
3541 || path.ends_with("::vars"))
3542 {
3543 return Some("Env");
3544 }
3545 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against dotenv 0.15.0 and dotenvy 0.15.7
3546 // (byte-identical shape in both crates) — the ITERATOR-returning siblings of the load verbs above,
3547 // which return the parsed pairs instead of mutating the environment but still touch disk to get
3548 // them: `dotenv_iter`/`from_filename_iter` (lib.rs) resolve and open a `.env`-shaped file exactly
3549 // like `dotenv`/`from_filename` two lines up (same `Finder::new().find()` call); `from_path_iter`
3550 // opens the given path directly. Bucketed `Env` for consistency with the rest of this family rather
3551 // than `Fs` (self-scan's raw signal): the crate's OWN purpose is env-var loading, and splitting the
3552 // iterator variants into a different effect bucket from their non-iterator siblings would be an
3553 // arbitrary distinction a reviewer gains nothing from.
3554 //
3555 // NOT included: `Finder::find` / the free fn `find` (find.rs) — both are used only via a PRIVATE
3556 // `use crate::find::Finder;` (no `pub use`) inside a private `mod find;` (lib.rs), so neither
3557 // `dotenv(y)::Finder` nor `dotenv(y)::find` is a path any external consumer can compile a call to;
3558 // the ratchet's generator still guesses a crate-root alias for every candidate (the same private-
3559 // module shape as tempfile's `imp::` functions above). Removed from open.tsv rather than guessed.
3560 if matches!(crate_name, "dotenvy" | "dotenv")
3561 && (path.ends_with("::dotenv_iter")
3562 || path.ends_with("::from_filename_iter")
3563 || path.ends_with("::from_path_iter"))
3564 {
3565 return Some("Env");
3566 }
3567 // Wall-clock reads. Match the `now` accessor precisely (ends_with), not any path
3568 // containing the substring "now". The `time` crate (distinct from `std::time`/`chrono`)
3569 // reads the clock via `now_utc`/`now_local` (and the deprecated `Instant::now`).
3570 if (crate_name == "chrono" || path.starts_with("std::time::")) && path.ends_with("::now") {
3571 return Some("Clock");
3572 }
3573 // SOUNDNESS R1528 — chrono's deprecated `Utc::today()` / `Local::today()` are `Utc::now().date()` /
3574 // `Local::now().date()` (offset/utc.rs:63, offset/local/mod.rs:133): the wall clock, spelled without the
3575 // `now` the rule above matches. Exact receivers only — `today` is not claimed for any other chrono type.
3576 // EXECUTED 2026-11-08: both return the current date. Their absence read PURE at every consumer.
3577 if crate_name == "chrono" && (path.ends_with("Utc::today") || path.ends_with("Local::today")) {
3578 return Some("Clock");
3579 }
3580 // SOUNDNESS R1528 — chrono 0.4.45 `Local`'s four `TimeZone` offset lookups (offset/local/mod.rs:177-194 ->
3581 // unix.rs `offset`): each goes through the per-thread `Cache`, whose `offset` calls `SystemTime::now()` on EVERY
3582 // lookup to decide whether the cached zone is older than a second, and whose first use per thread (and every
3583 // refresh) reads `TZ` and `/etc/localtime` (or the file `TZ` names). EXECUTED (fx_chrono): the offset follows `TZ`
3584 // and an unset `TZ` follows the system file; a mid-run `TZ` change is NOT seen for the first second and IS seen
3585 // after 1.2s, i.e. the wall clock gates the re-read. `Local::now` (above) is `Utc::now().with_timezone(&Local)`
3586 // and carries the same single `Clock`, so these are charged the one effect the call makes unconditionally.
3587 // KNOWN GAP, not a choice: `classify` carries ONE effect per path and the real set is Clock + Env + Fs (Env/Fs
3588 // once per thread and per refresh), so `deny Env`/`deny Fs` over a consumer of these still passes — filed with
3589 // the coordinator. Only the concrete `Local` receiver is claimed: the generic `TimeZone::offset_from_*` and
3590 // `DateTime::with_timezone<Tz>` forwarders are the type argument's effect (R1557, `OPEN_ENTRY_VERDICTS`).
3591 if crate_name == "chrono" {
3592 if let Some((ty, m)) = type_method(path, "chrono", &["", "offset", "offset::local"]) {
3593 if ty == "Local"
3594 && matches!(m, "offset_from_utc_datetime" | "offset_from_local_datetime" | "offset_from_utc_date"
3595 | "offset_from_local_date")
3596 {
3597 return Some("Clock");
3598 }
3599 // SOUNDNESS R1616 (provided-member half) — `TimeZone`'s PROVIDED members on `Local`. Their default bodies
3600 // (offset/mod.rs) call `self.offset_from_*` (directly, or through `from_utc_datetime`/`from_local_datetime`/
3601 // `from_local_date`), which `Local` implements with the lookups charged above; `Local` overrides none of them.
3602 // The consumer's typed call is `chrono::Local::from_utc_datetime`, and with no row the calibrated crate read it
3603 // PURE: `chrono::Local.from_utc_datetime(n)` was ABSENT. EXECUTED (rustagent-recv `fx/c`, DYLD interposer), every
3604 // name below on a valid input: 2 clock reads + getenv TZ + open /etc/localtime. Same single-effect convention
3605 // (and the same Env/Fs gap) as the four lookups.
3606 if ty == "Local"
3607 && matches!(m, "from_utc_datetime" | "from_utc_date" | "from_local_datetime" | "from_local_date"
3608 | "with_ymd_and_hms" | "ymd" | "ymd_opt" | "yo" | "yo_opt" | "isoywd" | "isoywd_opt" | "timestamp"
3609 | "timestamp_opt" | "timestamp_millis" | "timestamp_millis_opt" | "timestamp_nanos" | "timestamp_micros"
3610 | "datetime_from_str")
3611 {
3612 return Some("Clock");
3613 }
3614 }
3615 }
3616 if crate_name == "time"
3617 && (path.ends_with("::now_utc") || path.ends_with("::now_local") || path.ends_with("::now"))
3618 {
3619 return Some("Clock");
3620 }
3621 // SOUNDNESS R1288 — THIRD-PARTY CLOCK WRAPPERS. `tokio::time::Instant::now()` reads the monotonic clock (through
3622 // tokio's mockable time source) exactly as `std::time::Instant::now()` does, and read PURE: tokio-util's
3623 // `DelayQueue::insert`/`reset` and tokio-stream's `throttle` were covered only by a false `ambiguous:` hedge in
3624 // v0.40.13. The same `now` accessor on the dedicated clock crates' instant types: `quanta` (`Instant::now`,
3625 // `Clock::now`), `minstant`, `web_time`, `instant`, `coarsetime` (`Instant::now`). `now` only — their arithmetic and
3626 // conversions are pure.
3627 if path.ends_with("::now")
3628 && (path.starts_with("tokio::time::Instant::")
3629 || matches!(crate_name, "quanta" | "minstant" | "web_time" | "instant" | "coarsetime"))
3630 {
3631 return Some("Clock");
3632 }
3633 // `tracing`: same principle as the `log` facade below — the crate's TYPES are pure data, so match
3634 // the emit, not the whole crate. The actual program output is the macro-expanded
3635 // `Subscriber::event`/`event!`/`Span::*enter*` dispatch and the `Span::new*`/`Span::record`
3636 // recording path that drives the subscriber. The data-type accessors — `Level::as_str`,
3637 // `Span::is_disabled`/`metadata`/`id`, and constructing/reading `Level`/`LevelFilter`/`Span`/
3638 // `Event`/`Metadata`/`Field`/`FieldSet`/`Id` — are PURE (no output is produced), so whole-crate Log
3639 // fabricated Log on them. Match the emit verbs; everything else returns None.
3640 if crate_name == "tracing" {
3641 let m = path.rsplit("::").next().unwrap_or(path);
3642 // The user-facing emit MACROS (`tracing::info!`/`warn!`/…) — candor-scan is pre-expansion, so it
3643 // sees the raw macro path `tracing::info`, not the expanded `__tracing`/`Subscriber::event` the
3644 // deep (post-expansion) engine sees. Only the macro names; the pure DATA types (Level/Span/Event)
3645 // have other tails and stay None.
3646 if m == "trace" || m == "debug" || m == "info" || m == "warn" || m == "error"
3647 || m == "trace_span" || m == "debug_span" || m == "info_span" || m == "warn_span"
3648 || m == "error_span" || m == "span"
3649 || m == "event"
3650 || m == "new_span"
3651 || m == "record"
3652 || m == "record_follows_from"
3653 || m == "enter"
3654 || m == "exit"
3655 || m == "in_scope"
3656 || m == "entered"
3657 || path.contains("::__macro_support")
3658 || path.contains("::__tracing")
3659 || path.contains("Subscriber::event")
3660 || path.contains("Subscriber::new_span")
3661 || path.contains("Subscriber::enter")
3662 || path.contains("Subscriber::exit")
3663 {
3664 return Some("Log");
3665 }
3666 return None;
3667 }
3668 // The `log` facade: its macros route through `log::__private_api`; the crate's types
3669 // (`Level`, `LevelFilter`) are pure, so match the logging entry, not the whole crate.
3670 if crate_name == "log" {
3671 // Expanded macro form (deep engine) OR the raw user-facing macro names (candor-scan, pre-expansion).
3672 // `log::Level`/`LevelFilter`/`Record`/`Metadata` have other tails, so the type surface stays pure.
3673 let m = path.rsplit("::").next().unwrap_or(path);
3674 if path.contains("::__private_api")
3675 || m == "error" || m == "warn" || m == "info" || m == "debug" || m == "trace" || m == "log"
3676 {
3677 return Some("Log");
3678 }
3679 }
3680 // Compiler diagnostic emission — the ONE genuinely effectful operation in the otherwise-pure
3681 // rustc_* surface (a dylint lint's actual OUTPUT: it writes warnings/errors to the compiler's
3682 // diagnostic sink). Classified `Log` (same family as `tracing`/`log` — program output). Match the
3683 // emission verbs precisely; rustc_lint/rustc_errors are mostly pure types (Lint, LintId, the Diag
3684 // BUILDERS), and only the terminal `emit`/`emit_span_lint` actually produces output.
3685 if crate_name == "rustc_lint"
3686 && (path.ends_with("::emit_span_lint")
3687 || path.ends_with("::span_lint")
3688 || path.ends_with("::span_lint_hir"))
3689 {
3690 return Some("Log");
3691 }
3692 if crate_name == "rustc_errors"
3693 && (path.ends_with("::emit")
3694 || path.ends_with("::emit_diagnostic")
3695 || path.ends_with("::emit_now"))
3696 {
3697 return Some("Log");
3698 }
3699 // arboard: the effectful surface is the `Clipboard` handle's read/write verbs (each talks to the
3700 // OS clipboard / X11/Wayland/Win32/NSPasteboard server). The data types — chiefly `arboard::Error`
3701 // (whose `Display`/`to_string` formatting is pure) and the `ImageData`/`GetExtLinux`/`SetExtLinux`
3702 // option types — are PURE, so whole-crate Clipboard fabricated Clipboard on e.g. an error
3703 // `to_string()`. Match the handle verbs; everything else returns None. `Clipboard::new` opens the
3704 // connection to the clipboard server, so it's an effect too; `get`/`set` return the
3705 // builder-then-read `Get`/`Set` cursors whose `text`/`image`/`html` terminals do the I/O.
3706 //
3707 // THE ~79-CRATE R59-CLASS AUDIT (2026-08-28), verified against arboard 3.6.1: `file_list`
3708 // (lib.rs:205,251, `Get::file_list`/`Set::file_list`) is the SAME builder-then-terminal shape as
3709 // `text`/`image`/`html` two lines up — reading/writing the clipboard's file-path list — but was
3710 // missing, not because it's ambiguous (it's the only `file_list` in the crate) but because
3711 // `eval/coverage-gate/generate.py`'s own generator only triggers on a self-scan `inferred` set
3712 // containing Fs/Net/Db/Exec; Clipboard isn't in that trigger set, so a missing Clipboard verb is
3713 // structurally invisible to the completeness gate regardless of phrasing. `Clear::default`
3714 // (lib.rs:265) is `Clipboard::clear`'s own documented alternate entry point
3715 // (`clear() { self.clear_with().default() }`, lib.rs:156-158) — the SAME "sibling constructor"
3716 // shape as `ignore::Walk::new` beside `WalkBuilder::build`; `clear_with()` itself doesn't share
3717 // `clear`'s leaf, so neither the constructor nor its terminal was covered. `default` is otherwise
3718 // unused in this crate (no derived `Default` impls), so the bare leaf carries no fabrication risk
3719 // once crate-gated.
3720 if crate_name == "arboard" {
3721 let m = path.rsplit("::").next().unwrap_or(path);
3722 if m == "new"
3723 || m == "get"
3724 || m == "set"
3725 || m == "clear"
3726 || m == "get_text"
3727 || m == "set_text"
3728 || m == "set_html"
3729 || m == "get_image"
3730 || m == "set_image"
3731 || m == "text"
3732 || m == "image"
3733 || m == "html"
3734 || m == "file_list"
3735 || m == "default"
3736 {
3737 return Some("Clipboard");
3738 }
3739 return None;
3740 }
3741 // ── Coverage-differential additions (calibrated against each crate's real API; see the per-crate
3742 // notes). All verb-keyed + crate-gated, with the pure builder/config/data surface returning None.
3743
3744 // `etcetera` — XDG/known-folder base+app directory resolution. Each dir ACCESSOR reads the
3745 // environment at call time (`$HOME`/`$XDG_*` on Unix, `%APPDATA%`/`%LOCALAPPDATA%` on Windows), and
3746 // the `choose_*`/`home_dir` entry points read `$HOME`. The `AppStrategyArgs` data struct and the
3747 // strategy types themselves are PURE. (Found DISCLOSED-but-unmodeled in 3/4 differential projects.)
3748 if crate_name == "etcetera" {
3749 let m = path.rsplit("::").next().unwrap_or(path);
3750 if m == "home_dir"
3751 || m == "choose_base_strategy" || m == "choose_native_strategy" || m == "choose_app_strategy"
3752 || m == "config_dir" || m == "data_dir" || m == "cache_dir"
3753 || m == "state_dir" || m == "runtime_dir" || m == "data_local_dir"
3754 {
3755 return Some("Env");
3756 }
3757 return None;
3758 }
3759 // `sqlx-core` (crate `sqlx_core`) — the execution terminals under the sqlx core (the `sqlx` builder
3760 // table maps `sqlx::query*`; here it's the core `Executor`/`Connection`/`Pool` round-trips). Opening
3761 // the connection is the network boundary (Net); the query/transaction round-trips are Db. The
3762 // `*Options`/query-builder/row data types are PURE. Crate-gated so the generic verbs never spread.
3763 if crate_name == "sqlx_core" {
3764 if path.ends_with("::connect") || path.ends_with("::connect_with") {
3765 return Some("Net");
3766 }
3767 // SOUNDNESS R1528 — sqlx-core 0.9.0, the pool / migrate / net / logger rows. See `sqlx_r1528`.
3768 if let Some(effect) = sqlx_r1528("sqlx_core", path) {
3769 return Some(effect);
3770 }
3771 if path.ends_with("::fetch") || path.ends_with("::fetch_all") || path.ends_with("::fetch_one")
3772 || path.ends_with("::fetch_optional") || path.ends_with("::fetch_many")
3773 || path.ends_with("::execute") || path.ends_with("::execute_many")
3774 || path.ends_with("::prepare") || path.ends_with("::prepare_with")
3775 || path.ends_with("::acquire") || path.ends_with("::begin") || path.ends_with("::ping")
3776 {
3777 return Some("Db");
3778 }
3779 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against sqlx-core 0.8.6/0.9.0 (identical
3780 // shape in both) — the free-function surface under `pub mod fs`/`pub mod net::tls`/
3781 // `pub mod migrate`, none of which end in the connect/fetch/execute verbs above:
3782 //
3783 // `fs::{read,read_to_string,create_dir_all,remove_file,remove_dir,remove_dir_all,read_dir}`
3784 // (fs.rs) are direct `std::fs::*` calls off-loaded to `spawn_blocking` — sqlx's OWN async
3785 // filesystem shim, used by the migrator to read `.sql` files and by SQLite to manage on-disk
3786 // databases. `net::tls::handshake` (net/tls/mod.rs:69) is the real TLS handshake dispatch
3787 // reqwest/ureq-style crates already model — sqlx has its OWN copy because it drives the
3788 // handshake over its own `Socket` trait rather than a `TcpStream` directly.
3789 // `migrate::resolve_blocking`/`resolve_blocking_with_config` (migrate/source.rs:151,156) are
3790 // `#[doc(hidden)]` but still `pub use`-exported at `sqlx_core::migrate` (mod.rs:16) — real,
3791 // callable, and reads the migrations directory off disk (`fs::read_dir` + per-file `canonicalize`).
3792 //
3793 // NOT included: `net::tls::tls_rustls::RustlsSocket::poll_{flush,read_ready,write_ready,
3794 // shutdown}` — `RustlsSocket` is declared inside `mod tls_rustls;` (net/tls/mod.rs), which is
3795 // PRIVATE with no re-export, so no external consumer can ever name it (the same private-module
3796 // shape as tempfile's `imp::` functions above). Removed from open.tsv rather than guessed.
3797 if path == "sqlx_core::fs::read"
3798 || path == "sqlx_core::fs::read_to_string"
3799 || path == "sqlx_core::fs::create_dir_all"
3800 || path == "sqlx_core::fs::remove_file"
3801 || path == "sqlx_core::fs::remove_dir"
3802 || path == "sqlx_core::fs::remove_dir_all"
3803 || path == "sqlx_core::fs::read_dir"
3804 || path.ends_with("::migrate::resolve_blocking")
3805 || path.ends_with("::migrate::resolve_blocking_with_config")
3806 || path == "sqlx_core::config::Config::try_from_path"
3807 || path == "sqlx_core::Config::try_from_path"
3808 || path == "sqlx_core::config::Config::try_from_path_or_default"
3809 || path == "sqlx_core::Config::try_from_path_or_default"
3810 || path == "sqlx_core::config::Config::try_from_crate_or_default"
3811 || path == "sqlx_core::Config::try_from_crate_or_default"
3812 {
3813 return Some("Fs");
3814 }
3815 if path.ends_with("::net::tls::handshake") {
3816 return Some("Net");
3817 }
3818 return None;
3819 }
3820 // `walkdir` — recursive directory traversal. Charged at `WalkDir::new` (construction), the SAME
3821 // point `ignore::WalkBuilder::build`/`glob::glob` are charged, and NOT at `IntoIter::next` (driving
3822 // the iterator) as this rule originally read. The disk read (`read_dir`+`stat`) is technically lazy
3823 // — nothing happens until the value is iterated — but `.next()` is reached ONLY through a receiver
3824 // typed as `walkdir::IntoIter`, and candor-scan's receiver-typing (`ctor_type`/`resolve_recv_type`)
3825 // hard-blocks the `.into_iter()` verb (a guard against fabricating onto a DIFFERENT std type, e.g.
3826 // `Vec::into_iter()` -> `std::vec::IntoIter`) with no per-crate exception for the SAME-crate case.
3827 // `WalkDir::into_iter()` returns `walkdir::IntoIter` (same crate as `WalkDir` itself, exactly the
3828 // shape the blocklist exists to protect, not the shape it should be blocking) — so every idiomatic
3829 // form (`for e in WalkDir::new(p)`, `.into_iter().count()`, `.into_iter().filter_map(..)`, an
3830 // untyped `let it = ...into_iter(); it.next()`) never reaches a typed `IntoIter::next` receiver and
3831 // read silent-pure under the OLD rule, with `deny Fs` exiting 0 over code that walks the filesystem.
3832 // Charging at `WalkDir::new` needs no receiver typing at all — it's a plain `Expr::Call`, robust to
3833 // however the iterator is later consumed. `max_depth`/`follow_links`/`sort_by`/`into_iter` (the
3834 // BUILDER chain, no I/O until pulled) and the cached `DirEntry::path`/`file_name`/`file_type`/`depth`
3835 // accessors (`file_type` makes NO syscall) stay PURE — matched by nothing here, so they fall through.
3836 // The `IntoIter::next`/`DirEntry::metadata` rule is KEPT, not dead: it still fires whenever the
3837 // concrete `walkdir::IntoIter` type reaches `.next()` by a route the blocklist doesn't gate — an
3838 // EXPLICIT type annotation (`let it: walkdir::IntoIter = ...`), a struct field declared with that
3839 // type, or a local fn's declared return type — real, if rarer, shapes than the inline-chain idiom.
3840 // (Companion to the already-modeled `ignore`.)
3841 if crate_name == "walkdir" {
3842 if path.ends_with("::WalkDir::new")
3843 || path.ends_with("::IntoIter::next")
3844 || path.ends_with("::DirEntry::metadata")
3845 {
3846 return Some("Fs");
3847 }
3848 return None;
3849 }
3850 // `filetime` — file-timestamp mutation. The `set_*` free fns issue utimes/utimensat/futimens (Fs).
3851 // `FileTime::now` reads the system clock (Clock). The `FileTime::from_*`/`zero` value constructors
3852 // (incl. `from_last_modification_time(&Metadata)` etc., which read an ALREADY-loaded `&Metadata`, not
3853 // the disk) and the `seconds`/`nanoseconds` accessors are PURE.
3854 if crate_name == "filetime" {
3855 if path.ends_with("::set_file_mtime") || path.ends_with("::set_file_atime")
3856 || path.ends_with("::set_file_times") || path.ends_with("::set_symlink_file_times")
3857 || path.ends_with("::set_file_handle_times")
3858 {
3859 return Some("Fs");
3860 }
3861 if path.ends_with("::FileTime::now") {
3862 return Some("Clock");
3863 }
3864 return None;
3865 }
3866 // `execute` — the `Execute` trait that extends `std::process::Command` with run helpers. The
3867 // `execute*` verbs SPAWN a child process (Exec). The `execute::command`/`shell` free fns and the
3868 // `command!`/`command_args!` macros only BUILD a Command (no spawn) and stay PURE.
3869 if crate_name == "execute" {
3870 if path.contains("::execute") {
3871 return Some("Exec");
3872 }
3873 return None;
3874 }
3875 // `ctrlc` — installs an OS signal handler (Unix SIGINT/SIGTERM/SIGHUP, Windows CTRL_C_EVENT) and
3876 // spawns its handler thread. Signals are an inter-process control channel, so the closest bucket is
3877 // Ipc (candor has no dedicated Signal effect; same judgment as routing SysV/pipe IPC to Ipc).
3878 if crate_name == "ctrlc" {
3879 if path.ends_with("::set_handler") || path.ends_with("::try_set_handler") {
3880 return Some("Ipc");
3881 }
3882 return None;
3883 }
3884 // `clap` — argument parsing. ONLY the terminals that read `std::env::args_os` at call time are an
3885 // effect (Env): `get_matches`/`get_matches_mut`/`try_get_matches` and the derive `parse`/`try_parse`.
3886 // clap is MOSTLY PURE: the ENTIRE builder surface (`Command::new`/`arg`/`about`/`Arg::new`) stays
3887 // None, and crucially the `*_from`/`*_parse_from` variants take an EXPLICIT iterator (they do NOT
3888 // read argv) so they stay pure too.
3889 //
3890 // THE ~79-CRATE R59-CLASS AUDIT (2026-08-28): this file used to leave `Arg::env` unmodeled, calling
3891 // bare `::env` "too generic to gate safely" — the same words used for libc's genuinely ambiguous fd
3892 // verbs. It isn't the same shape: `Arg::env(name)` calls `env::var_os(&name)` DIRECTLY at builder
3893 // time (verified against clap_builder 4.6.6, builder/arg.rs:2205-2213 — real, immediate, not
3894 // deferred to `get_matches()`), and — unlike a bare fd, which could be Fs/Net/Ipc — `clap_builder`
3895 // has exactly ONE `pub fn` ending `::env` in its entire source (`Arg::env`; the deprecated
3896 // `Arg::env_os` is a one-line delegate to it). This whole match arm is already crate-gated on
3897 // `crate_name == "clap"`, so no ambiguity survives to fabricate on: a fixture proved the untreated
3898 // gap silently vanishes the function ("functions": [] — the exact R59 shape, worse than an
3899 // uncalibrated dependency, since `clap` is fully `CALIBRATED_CRATES` and the miss reads as
3900 // reviewed-pure rather than disclosed). Classified, not carved into
3901 // `CALIBRATED_BUT_PARTIAL_CRATES`, because the effect IS unambiguous once actually checked.
3902 if crate_name == "clap" {
3903 if path.ends_with("::get_matches") || path.ends_with("::get_matches_mut")
3904 || path.ends_with("::try_get_matches")
3905 || path.ends_with("::parse") || path.ends_with("::try_parse")
3906 || path.ends_with("::env") || path.ends_with("::env_os")
3907 {
3908 return Some("Env");
3909 }
3910 return None;
3911 }
3912 // `jiff` — date/time. `Timestamp::now`/`Zoned::now`/`Zoned::now_with` read the wall clock (Clock).
3913 // `tz::TimeZone::system`/`get` and `tz::db().get` read the system tzdb files from disk
3914 // (`/etc/localtime`, `/usr/share/zoneinfo`; `system` is also `$TZ`-overridable — Fs is the dominant
3915 // op, modeled as Fs). The `Span`/`civil` date math and `Timestamp`/`Zoned` arithmetic are PURE.
3916 if crate_name == "jiff" {
3917 if path.ends_with("::now") || path.ends_with("::now_with") {
3918 return Some("Clock");
3919 }
3920 if path.ends_with("::TimeZone::system") || path.ends_with("::TimeZone::get")
3921 || path.ends_with("::TimeZoneDatabase::get")
3922 // `TimeZone::try_system` (tz/timezone.rs:391) is `TimeZone::system`'s fallible sibling —
3923 // same `crate::tz::system::get(crate::tz::db())` call, real localtime/zoneinfo disk read.
3924 || path.ends_with("::TimeZone::try_system")
3925 {
3926 return Some("Fs");
3927 }
3928 // SOUNDNESS R1528 — jiff 0.2.38 (above the 0.2.35 ceiling; the rules still apply to any version). Every row
3929 // below was read in 0.2.38 and EXECUTED against a consumer fixture whose `TZDIR` names a directory holding
3930 // ONE zone file `Fake/Zone` (a copy of Asia/Tokyo): the lookup succeeds with that file's offset when the
3931 // directory exists and errors when it is empty or `TZDIR` is unset, so the answer comes from the disk.
3932 // * `tz::db()` (tz/db/mod.rs:31) is a `OnceLock` whose initialiser is `TimeZoneDatabase::from_env`: it
3933 // reads `TZDIR` and opens the zoneinfo directories. `from_env`, `from_dir(path)` (opens the directory,
3934 // `ZoneInfoNames::new`) and `from_concatenated_path(path)` (opens the file, `Names::new`) are the database
3935 // CONSTRUCTORS; executed, `from_dir`/`from_concatenated_path` return `Err` for a missing path.
3936 // * `Date::in_tz`, `DateTime::in_tz`, `Timestamp::in_tz`, `Zoned::in_tz` are `tz::db().get(name)` plus a
3937 // conversion (civil/date.rs:1114, civil/datetime.rs:1446, timestamp.rs:1263, zoned.rs:655).
3938 // * `DateTimeParser::{parse_zoned, parse_time_zone}`, `BrokenDownTime::to_zoned`, `Pieces::to_time_zone` and
3939 // `TimeZoneAnnotation::to_time_zone` are `<same>_with(crate::tz::db(), ..)`: the global database is an
3940 // ARGUMENT evaluated before the body branches, so it is initialised (TZDIR read, directory opened) even
3941 // for an offset-only annotation that never looks a name up. Fs is therefore unconditional on first use.
3942 // Single effect, as the `TimeZone::get` rule above: the `TZDIR` Env read rides with the same call. NOT claimed:
3943 // the three `_with(db)` variants (the effect is the caller-supplied database's, charged at the constructor
3944 // above; a `bundled()` database makes the same call pure — EXECUTED) and `TimeZoneDatabase::bundled` (a
3945 // `warn!` only with jiff's non-default `logging` feature AND an empty bundle) — filed as R1559.
3946 if let Some((ty, m)) = type_method(
3947 path,
3948 "jiff",
3949 &["", "civil", "civil::date", "civil::datetime", "timestamp", "zoned", "tz", "tz::db", "fmt", "fmt::strtime",
3950 "fmt::temporal", "fmt::temporal::pieces", "fmt::temporal::mod"],
3951 ) {
3952 if matches!(
3953 (ty, m),
3954 ("Date" | "DateTime" | "Timestamp" | "Zoned", "in_tz")
3955 | ("TimeZoneDatabase", "from_env" | "from_dir" | "from_concatenated_path")
3956 | ("DateTimeParser", "parse_zoned" | "parse_time_zone")
3957 | ("BrokenDownTime", "to_zoned")
3958 | ("Pieces" | "TimeZoneAnnotation", "to_time_zone")
3959 ) {
3960 return Some("Fs");
3961 }
3962 }
3963 if path == "jiff::db" || path == "jiff::tz::db" || path == "jiff::tz::db::db" {
3964 return Some("Fs");
3965 }
3966 return None;
3967 }
3968 // `env_logger` — installs the global logger and emits to stderr; reads `RUST_LOG`/`RUST_LOG_STYLE`.
3969 // The init terminals are the effect (Log — program output, same family as `log`/`tracing`). The
3970 // `Builder::new`/`build` and the format/filter/target config setters are PURE.
3971 if crate_name == "env_logger" {
3972 if path.ends_with("::init") || path.ends_with("::try_init")
3973 || path.ends_with("::init_from_env") || path.ends_with("::try_init_from_env")
3974 {
3975 return Some("Log");
3976 }
3977 return None;
3978 }
3979 // `dialoguer` — interactive terminal prompts. The `interact*` verbs read stdin + write the tty (a
3980 // console dialogue with the user — Ipc, like the other local-channel effects). The
3981 // `with_prompt`/`default`/`items`/`validate_with` BUILDERS are PURE.
3982 if crate_name == "dialoguer" {
3983 if path.ends_with("::interact") || path.ends_with("::interact_on")
3984 || path.ends_with("::interact_text") || path.ends_with("::interact_text_on")
3985 || path.ends_with("::interact_opt") || path.ends_with("::interact_on_opt")
3986 {
3987 return Some("Ipc");
3988 }
3989 // `Editor::edit` (edit.rs:93) writes the caller's string to a real tempfile, spawns
3990 // `$VISUAL`/`$EDITOR` on it via `process::Command::spawn().wait()`, then reads the result back
3991 // — a real subprocess launch, not a terminal-dialogue read/write like the verbs above.
3992 if path.ends_with("Editor::edit") {
3993 return Some("Exec");
3994 }
3995 // WIDENED-CORE AUDIT (2026-08-28): `Editor::new`/`Editor::default` (edit.rs:54,44) call
3996 // `get_default_editor()` (edit.rs:31), which reads `env::var_os("VISUAL")` then `("EDITOR")`
3997 // IMMEDIATELY at construction — a real, independent Env read a consumer triggers just by
3998 // building an `Editor`, whether or not `.edit()` is ever called (the SAME "builder-time env
3999 // read, separate from the terminal verb" shape as `clap::Arg::env`, R59-class). Verified against
4000 // dialoguer 0.12.0: `get_default_editor` is the ONLY caller of either `env::var_os`, so there is
4001 // no ambiguity gating this to just these two constructors.
4002 if path.ends_with("Editor::new") || path.ends_with("Editor::default") {
4003 return Some("Env");
4004 }
4005 return None;
4006 }
4007 // `tracing_subscriber` — the subscriber that gives `tracing` somewhere to go. TWO effects, and the
4008 // filing said "Log/Fs": VERIFIED against 0.3.23, the Fs half is WRONG.
4009 //
4010 // Log — `fmt/fmt_layer.rs:749` defaults `make_writer: io::stdout`, so the fmt INIT terminals install
4011 // a subscriber that writes program output. Same family as `log`/`tracing`/`env_logger`.
4012 // Env — `fmt/mod.rs:1219` reads `RUST_LOG` on the `init()` path, `fmt_layer.rs` reads `NO_COLOR`,
4013 // and `filter/env/builder.rs:189,203` read `env::var(self.env_var_name())`.
4014 //
4015 // NOT Fs. The only `std::fs` in the crate is `impl MakeWriter for std::fs::File` — the crate ACCEPTING
4016 // a caller-supplied File, not opening one. The caller's `File::create` is classified on the caller, so
4017 // charging Fs here would double-count, exactly the `serde_json::from_reader` caveat one crate over.
4018 //
4019 // The builders (`fmt()`, `layer()`, `with_writer`, `with_target`, `EnvFilter::new`) are PURE: they
4020 // describe a subscriber. Only the INIT terminals install one, and only the from-env constructors read.
4021 if crate_name == "tracing_subscriber" {
4022 if path.ends_with("::init") || path.ends_with("::try_init") {
4023 return Some("Log");
4024 }
4025 // SOUNDNESS R1528 — `fmt::Layer::new()` is `Self::default()`, and `Default for Layer`
4026 // (fmt/fmt_layer.rs:743) reads `env::var("NO_COLOR")` to decide ANSI. EXECUTED 2026-11-08: the
4027 // same program emitted ANSI escapes with NO_COLOR unset and none with `NO_COLOR=1`, so the
4028 // layer's behaviour depends on the environment at construction. Both module spellings are
4029 // matched; `tracing_subscriber::Layer` at the crate root is the TRAIT, which has no `new`.
4030 // NOT claimed here: `fmt::layer()` and `Layer::default()` reach the same read, but a rule
4031 // for them contradicts the assertion below that `fmt::layer` is pure — filed for decision.
4032 if path.ends_with("::fmt::Layer::new") || path.ends_with("::fmt_layer::Layer::new") {
4033 return Some("Env");
4034 }
4035 // SOUNDNESS R1528 — the rest of the `NO_COLOR` family. `Layer::new` is `Self::default()` and every one of
4036 // `fmt::layer()` (fmt/mod.rs:337), `fmt::Layer::default()` (fmt_layer.rs:739), `fmt::fmt()` (mod.rs:324),
4037 // `fmt::Subscriber::builder()` (:352) and `SubscriberBuilder::default()`/`Subscriber::default()` (:467/:362) reach
4038 // the same `env::var("NO_COLOR")` through `Layer::default`. EXECUTED (fx_ts, `layer`/`builders`): each emitted
4039 // ANSI escapes with `NO_COLOR` unset and none with `NO_COLOR=1`. This REVERSES the earlier "the builders are
4040 // PURE" reading in the comment above and the `fmt::layer` assertion that went with it: that reading missed the
4041 // `Default` dispatch. The setters (`with_target`, `with_writer`, ...) stay pure — they only move the value.
4042 if path == "tracing_subscriber::fmt::layer" || path == "tracing_subscriber::fmt::fmt"
4043 || path == "tracing_subscriber::fmt"
4044 || path.ends_with("::fmt::Layer::default") || path.ends_with("::fmt_layer::Layer::default")
4045 || path == "tracing_subscriber::FmtSubscriber::builder" || path.ends_with("::fmt::Subscriber::builder")
4046 || path.ends_with("::fmt::Subscriber::default") || path.ends_with("::fmt::SubscriberBuilder::default")
4047 {
4048 return Some("Env");
4049 }
4050 // SOUNDNESS R1528 — the `FormatTime` implementors that READ THE CLOCK when formatting (fmt/time/mod.rs:136,
4051 // time_crate.rs, chrono_crate.rs: `SystemTime::now()`, `OffsetDateTime::now_{utc,local}()`, `chrono::{Utc,
4052 // Local}::now()`), and `Uptime`'s constructors, whose epoch is `Instant::now()` (mod.rs:85,122). EXECUTED (fx_ts,
4053 // `times`): each formatter printed a different timestamp 1.1s apart; `uptime()` printed the time elapsed since the
4054 // call. `Uptime::format_time` itself only subtracts (`epoch.elapsed()` — a clock read, but the unit is not a
4055 // row) and `From<Instant>` and `time()` (a unit struct) read nothing: NOT claimed.
4056 if let Some((ty, m)) = type_method(
4057 path,
4058 "tracing_subscriber",
4059 &["", "fmt", "fmt::time", "fmt::time::time_crate", "fmt::time::chrono_crate", "time"],
4060 ) {
4061 // `OffsetTime::local_rfc_3339` (tracing-subscriber 0.3.23, the refresh's drift row): `Self::new(UtcOffset::
4062 // current_local_offset()?, Rfc3339)` (fmt/time/time_crate.rs:363), the `time` crate's local-offset probe that this
4063 // file already charges `Clock` + `Fs` (R1612). EXECUTED (macOS, DYLD interposer, TZ unset): 1 `clock_gettime` +
4064 // `access(/etc/localtime)`; a pure control in the same harness 0.
4065 if (m == "format_time"
4066 && matches!(ty, "SystemTime" | "UtcTime" | "LocalTime" | "OffsetTime" | "ChronoLocal" | "ChronoUtc"))
4067 || (ty == "Uptime" && m == "default")
4068 || (ty == "OffsetTime" && m == "local_rfc_3339")
4069 {
4070 return Some("Clock");
4071 }
4072 }
4073 if path == "tracing_subscriber::uptime" || path == "tracing_subscriber::fmt::uptime"
4074 || path == "tracing_subscriber::fmt::time::uptime"
4075 {
4076 return Some("Clock");
4077 }
4078 if path.ends_with("::from_default_env") || path.ends_with("::try_from_default_env")
4079 || path.ends_with("::from_env") || path.ends_with("::from_env_lossy")
4080 || path.ends_with("::try_from_env")
4081 {
4082 return Some("Env");
4083 }
4084 return None;
4085 }
4086 // `crossterm` — the terminal driver. The tty is a USER DIALOGUE CHANNEL, so this is Ipc, matching the
4087 // ruling `dialoguer`/`console`/`terminal_colorsaurus` already carry rather than a new one.
4088 //
4089 // VERIFIED against crossterm-0.28.1 rather than assumed: `command.rs` `execute`/`queue` end in
4090 // `self.flush()?` on the writer (real code, not a doc example), `event::read`/`poll` read tty input,
4091 // and `terminal::{enable,disable}_raw_mode` + `size`/`window_size` talk to the device.
4092 //
4093 // `size`/`window_size`/`is_raw_mode_enabled` ARE classified, and that is deliberate: once a crate is
4094 // CALIBRATED every unmatched path becomes a PURITY CLAIM rather than a disclosed blind spot, so a tty
4095 // ioctl left to fall through would be claimed pure. The genuinely pure surface — the Command VALUE
4096 // types (`Print`, `MoveTo`, `SetForegroundColor`), the style/event data types — carries none of these
4097 // tails and stays pure correctly.
4098 if crate_name == "crossterm" {
4099 // SOUNDNESS R1528 (b10) — crossterm 0.29.0 (above its 0.28.1 ceiling). See `crossterm_r1528`.
4100 if let Some(effect) = crossterm_r1528(path) {
4101 return Some(effect);
4102 }
4103 if path.ends_with("::execute") || path.ends_with("::queue")
4104 || path.ends_with("::event::read") || path.ends_with("::event::poll")
4105 || path.ends_with("::enable_raw_mode") || path.ends_with("::disable_raw_mode")
4106 || path.ends_with("::size") || path.ends_with("::window_size")
4107 || path.ends_with("::is_raw_mode_enabled")
4108 {
4109 return Some("Ipc");
4110 }
4111 // `terminal::supports_keyboard_enhancement` (terminal/sys/unix.rs:188, re-exported `pub use
4112 // sys::supports_keyboard_enhancement` in terminal.rs) drives the SAME channel as
4113 // `event::read`/`enable_raw_mode` above (it toggles raw mode, then reads/polls terminal events
4114 // to detect the kitty keyboard protocol) — bucketed `Ipc` for consistency with the rest of this
4115 // tty-dialogue family rather than the `Fs` self-scan's raw signal reports (that signal traces
4116 // through the crate's internal event source opening `/dev/tty` as a fallback fd, the same
4117 // primitive `tty_fd` below wraps — not a distinct effect from the terminal channel itself).
4118 //
4119 // NOT modelled: `tty_fd` (terminal/sys/file_descriptor.rs:124) — declared inside `pub(crate) mod
4120 // sys;` (terminal.rs:99), so despite being `pub fn` it is unreachable from outside the crate
4121 // (only used internally by the event source). Removed from open.tsv rather than guessed.
4122 if path.ends_with("::supports_keyboard_enhancement") {
4123 return Some("Ipc");
4124 }
4125 return None;
4126 }
4127 // `ratatui` — the TUI renderer, and the single loudest source of disclosed-blind calls measured in the
4128 // 2026-07-14 four-ecosystem sweep (3,345 across three real repos). The backlog filed it as
4129 // "mark reviewed-pure"; VERIFYING against ratatui-0.29.0 REFUTES that for part of the surface:
4130 // `terminal/terminal.rs` `draw`/`flush`/`clear`/`autoresize`/`hide_cursor`/`show_cursor` end in a
4131 // backend flush, and `backend/` writes to the terminal. Marking the whole crate pure would have
4132 // claimed purity over the one API that actually writes.
4133 //
4134 // So the split is where the sweep's noise actually is: the BULK of those 3,345 calls are widget,
4135 // layout, buffer, style and text constructors — genuinely pure, and now covered rather than disclosed.
4136 // The Terminal/backend verbs are Ipc, same channel as crossterm underneath them.
4137 if crate_name == "ratatui" {
4138 // CARVE-OUT FIRST: `widgets::canvas` is an IN-MEMORY grid. `Context::draw(&shape)` sets
4139 // `self.dirty` and paints into a `Painter` — no terminal, no writer, provably pure — but it ends
4140 // in `::draw` and the tails below would have charged it `Ipc`. MEASURED as a live fabrication on a
4141 // fixture (`plot(ctx) -> ['Ipc']`) before this line existed, and it is a HOT path: a TUI drawing
4142 // charts or maps calls it per shape per frame.
4143 //
4144 // A DENYLIST (carve out the proven-pure module) rather than an allowlist of `Terminal::`, per the
4145 // family rule: an allowlist silently under-reports whatever it forgot, and the write surface here
4146 // is Terminal AND the backends (`CrosstermBackend::flush`), so pinning to `Terminal::` would drop
4147 // a direct backend call. Reading the crate, canvas is the only module whose methods collide with
4148 // these tails.
4149 if path.contains("::canvas::") {
4150 return None;
4151 }
4152 if path.ends_with("::draw") || path.ends_with("::try_draw") || path.ends_with("::flush")
4153 || path.ends_with("::autoresize") || path.ends_with("::clear")
4154 || path.ends_with("::hide_cursor") || path.ends_with("::show_cursor")
4155 || path.ends_with("::insert_before")
4156 || path.ends_with("::set_cursor_position") || path.ends_with("::get_cursor_position")
4157 {
4158 return Some("Ipc");
4159 }
4160 return None;
4161 }
4162 // `console` — terminal handle + styling. The `Term` read/write verbs do tty I/O (Ipc, the user
4163 // dialogue channel; note there is NO `write_str` — `Term` impls `io::Write`). The free-fn terminal
4164 // detection (`colors_enabled`/`user_attended`) reads `CLICOLOR`/`CLICOLOR_FORCE` (Env). The `Style`
4165 // color/format methods and the text utils (`strip_ansi_codes`/`pad_str`/`measure_text_width`) are PURE.
4166 //
4167 // THE ~79-CRATE R59-CLASS AUDIT (2026-08-28): the note above ("no `write_str` — `Term` impls
4168 // `io::Write`") RECORDED the trait impl without covering it. `Term::write`/`Term::flush` (console
4169 // 0.15.11, term.rs:622-633) call `self.write_through(buf)` — the SAME primitive `write_line` already
4170 // charges Ipc for — and `Term::read` (term.rs:650-654) calls `io::stdin().read(buf)` directly. These
4171 // are a second, real entry point to the identical tty channel, missed because `read`/`write`/`flush`
4172 // are the names every I/O type shares — but crate-gated on `crate_name == "console"`, term.rs
4173 // defines exactly one of each (verified against the real source; no ambiguity survives the gate,
4174 // same shape as `clap::Arg::env`). Left unclassified, a fn calling only `term.write_all(..)` (no
4175 // `write_line`) read as reviewed-pure, since `console` is fully `CALIBRATED_CRATES`.
4176 if crate_name == "console" {
4177 if path.ends_with("::write_line") || path.ends_with("::read_line")
4178 || path.ends_with("::read_line_initial_text") || path.ends_with("::read_char")
4179 || path.ends_with("::read_key") || path.ends_with("::read_key_raw")
4180 || path.ends_with("::read_secure_line")
4181 || path.ends_with("Term::write") || path.ends_with("Term::flush")
4182 || path.ends_with("Term::read")
4183 {
4184 return Some("Ipc");
4185 }
4186 if path.ends_with("::colors_enabled") || path.ends_with("::colors_enabled_stderr")
4187 || path.ends_with("::user_attended") || path.ends_with("::user_attended_stderr")
4188 {
4189 return Some("Env");
4190 }
4191 return None;
4192 }
4193 // `terminal_colorsaurus` — queries the terminal's colours by writing OSC 10/11 escapes and reading the
4194 // reply (bidirectional tty dialogue — Ipc, consistent with dialoguer/console). Nothing else is I/O.
4195 if crate_name == "terminal_colorsaurus" {
4196 if path.ends_with("::background_color") || path.ends_with("::foreground_color")
4197 || path.ends_with("::color_palette")
4198 // SOUNDNESS R321 — FQN-exact, not a suffix. `1.0.3` added
4199 // `ColorPalette::theme_mode(&self)` (src/lib.rs:112), a PURE derived accessor
4200 // comparing `perceived_lightness()` on two already-fetched colours, alongside the
4201 // effectful free `theme_mode(options)` (src/lib.rs:157). A `::theme_mode` suffix
4202 // charges `Ipc` to both. Counterpart to R319: the same upstream mechanism — a
4203 // release ADDING a name — under-reports in one crate and fabricates in another,
4204 // depending only on whether the new name is effectful.
4205 || path == "terminal_colorsaurus::theme_mode"
4206 {
4207 return Some("Ipc");
4208 }
4209 return None;
4210 }
4211 // `backoff` — retry-with-backoff. `retry`/`retry_notify` consult the clock and `thread::sleep`
4212 // between attempts (Clock). The `ExponentialBackoff`/builder config is PURE. (The user closure's own
4213 // effects are out of scope here — we model only backoff's own Clock effect.)
4214 if crate_name == "backoff" {
4215 if path.ends_with("::retry") || path.ends_with("::retry_notify") {
4216 return Some("Clock");
4217 }
4218 return None;
4219 }
4220 // `lscolors` — LS_COLORS parsing. ONLY `from_env` reads the environment (Env). `from_string`/
4221 // `style_for_path`/`style_for*` and the `Style` type take explicit input and are PURE.
4222 if crate_name == "lscolors" {
4223 if path.ends_with("::from_env") {
4224 return Some("Env");
4225 }
4226 return None;
4227 }
4228 // `wild` — argv with glob expansion. `args`/`args_os` read `std::env::args(_os)` (Env). Nothing else.
4229 if crate_name == "wild" {
4230 if path.ends_with("::args") || path.ends_with("::args_os") {
4231 return Some("Env");
4232 }
4233 return None;
4234 }
4235 // `grep_cli` — only the firm effect is modeled: `CommandReaderBuilder::build` spawns a child process
4236 // (Exec). The `is_readable_stdin`/`is_tty_*` fd probes (isatty/fstat on the std descriptors) are
4237 // deliberately NOT modeled — candor doesn't classify `IsTerminal`/isatty as an effect anywhere, and
4238 // they read no data; flagging them would be an inconsistent over-report.
4239 if crate_name == "grep_cli" {
4240 if path.ends_with("::build") {
4241 return Some("Exec");
4242 }
4243 // THE COVERAGE-GATE SWEEP (2026-08-27), verified against grep-cli 0.1.12 — the sibling entry
4244 // points to `::build` above, none of which end in that suffix:
4245 //
4246 // `CommandReader::new` (process.rs:195) is `CommandReaderBuilder::new().build(cmd)` — the same
4247 // spawn, reached through the type's own one-call constructor instead of the builder.
4248 // `CommandReader::close` (process.rs:218) drops the child's stdout handle then calls
4249 // `self.child.wait()` — `std::process::Child::wait` is ALREADY classified `Exec` elsewhere in
4250 // this file (reaping/blocking on a spawned child is part of the subprocess lifecycle this
4251 // project tracks), and self-scan resolves `self.child`'s concrete `process::Child` type to
4252 // confirm it, not a guess.
4253 // `DecompressionMatcher::command` (decompress.rs:179) builds a REAL `Command::new(&decomp_cmd.bin)`
4254 // naming an actual decompressor binary (gzip/xz/bzip2/…) and returns it un-spawned — the same
4255 // "constructs a real argv, caller spawns it" shape `Cred::credential_helper` was classified for
4256 // in git2 (see the FQN-exact rule near this file's top).
4257 // `DecompressionReader::new` (decompress.rs:352) is `DecompressionReaderBuilder::new().build(path)`,
4258 // which calls the already-covered `CommandReaderBuilder::build` two hops down.
4259 // `patterns_from_path` (pattern.rs:82) opens and reads the given file directly (`std::fs::File::
4260 // open`) — unrelated to the Exec family above, genuinely Fs.
4261 // SOUNDNESS R342 — `DecompressionReader::close` was the sibling this enumeration missed. It is
4262 // `match self.rdr { Ok(ref mut rdr) => rdr.close(), Err(_) => Ok(()) }` (`decompress.rs:383`),
4263 // i.e. the already-listed `CommandReader::close` one level up, so it reaps the same child. Its
4264 // `CommandReader` counterpart was listed and it was not — measured on a consumer fixture:
4265 // `CommandReader::close` charged `Exec` and `DecompressionReader::close` read ABSENT.
4266 //
4267 // The `read` pair is listed for the same reason one hop over: reading a `DecompressionReader`
4268 // or a `CommandReader` pulls bytes from the child's stdout, which is the point of having
4269 // spawned it. FQN-exact rather than a `::read`/`::close` suffix, because both are far too
4270 // common a leaf to match crate-wide — the R321 lesson.
4271 //
4272 // Found because R185 taught the scanner to see through `match self.rdr { Ok(..) }`, which made
4273 // grep-cli's own self-scan nominate it. The rule is what a CONSUMER needs; the self-scan only
4274 // pointed at it.
4275 if path == "grep_cli::CommandReader::new"
4276 || path == "grep_cli::CommandReader::close"
4277 || path == "grep_cli::CommandReader::read"
4278 || path == "grep_cli::DecompressionMatcher::command"
4279 || path == "grep_cli::DecompressionReader::new"
4280 || path == "grep_cli::DecompressionReader::close"
4281 || path == "grep_cli::DecompressionReader::read"
4282 || path == "grep_cli::decompress::DecompressionReader::close"
4283 || path == "grep_cli::decompress::DecompressionReader::read"
4284 || path == "grep_cli::process::CommandReader::close"
4285 || path == "grep_cli::process::CommandReader::read"
4286 {
4287 return Some("Exec");
4288 }
4289 if path == "grep_cli::patterns_from_path" {
4290 return Some("Fs");
4291 }
4292 return None;
4293 }
4294 // `clircle` — detects whether two handles are the same file (cycle protection). `Identifier::try_from`
4295 // (File/Stdio) issues an `fstat`, and `surely_conflicts_with` does an `lseek` (`stream_position`) — both
4296 // Fs. The `PartialEq`/`Hash` comparisons read stored dev/ino and are PURE. (The named methods
4297 // `are_identical`/`same_file` do NOT exist in the crate — not modeled.)
4298 if crate_name == "clircle" {
4299 if path.ends_with("::try_from") || path.ends_with("::surely_conflicts_with") {
4300 return Some("Fs");
4301 }
4302 return None;
4303 }
4304 None
4305}
4306
4307pub fn cap_from_name(name: &str) -> Option<&'static str> {
4308 EFFECTS.iter().copied().find(|e| *e == name)
4309}
4310
4311/// Refine the `Exec` cliff (spec §4 ⟨0.5⟩): the effects a *literal, statically-known* subprocess
4312/// head implies, matched by basename (`/usr/bin/curl` → `curl`). The head's effects are ADDED to a
4313/// caller that already carries `Exec` (a subprocess is still spawned — `Exec` is never dropped); an
4314/// unrecognised or dynamically-built head returns `&[]` and keeps the bare cliff (never guess). A
4315/// **candor engine** reads `Fs`/`Env` only — spec §7 item 12 (the analyzer self-boundary) guarantees
4316/// that, so that case is spec-supplied, not curation. The rest is a small curated table under the
4317/// same under-report rule as the crate classifier. INVARIANT: every head here is an external tool
4318/// that does NOT run the analysed project's own code (so `make`/`npm`/`cargo` are deliberately
4319/// absent — they stay the cliff). The reference engines share this table so the `Exec` boundary —
4320/// the one boundary every engine hits — refines identically (the §4-consistency argument).
4321/// SPEC §2 `fs` — for a call ALREADY classified `Fs`, the read/write direction its path implies.
4322/// `["read"]`, `["write"]`, `["read","write"]`, or `[]` when the verb does not say.
4323///
4324/// THE EMPTY CASE IS THE DISCIPLINE. §2: *"when `Fs` is reached but its kind is unknown … the field MUST
4325/// be omitted rather than guessed. An empty or partial `fs` would be read as a positive claim ('reads but
4326/// never writes'), which is the §4 trust contract's forbidden direction."* So an unrecognised verb
4327/// contributes nothing and the field stays absent; absence means "kind undetermined", never "read-only".
4328///
4329/// A syntactic refinement of an effect already proved, NOT a soundness claim. Deliberately the same
4330/// vocabulary as candor-java's `fsKind`, candor-swift's and candor-ts's — the surface is spec'd four-way,
4331/// and four engines inventing four verb tables for one field is how a shared field stops meaning one thing.
4332pub fn fs_kind(path: &str) -> &'static [&'static str] {
4333 // the terminal segment is the verb (`std::fs::write`, `File::create`, `f.read_to_string`)
4334 let leaf = path.rsplit("::").next().unwrap_or(path);
4335 // `OpenOptions::open` is the one verb that does NOT carry its own direction — the direction was set
4336 // by the BUILDER chain (`.read(true)`/`.write(true)`), which this function cannot see. The READ list
4337 // below holds a bare `open` for `File::open`, unambiguously a read; letting an `OpenOptions` receiver
4338 // reach it publishes `fs: ["read"]` for a builder configured `write(true)`, and §2 reads that as the
4339 // positive claim "reads but never writes" — the forbidden direction. The paragraph under the READ
4340 // list has always SAID this; nothing implemented it, and it only became REACHABLE when
4341 // `OpenOptions::open` started classifying on its own — before, the `Fs` came from the
4342 // `OpenOptions::new` constructor, whose leaf claims nothing, so the field was simply absent.
4343 // MEASURED on this commit's own fix before this guard: `OpenOptions::new().read(true).open(p)` went
4344 // from `fs` ABSENT to `fs: ["read"]`, i.e. the fix minted a claim the engine cannot support. Keyed on
4345 // the TYPE segment, so it holds for the `fs_err`/`tokio::fs`/`async_std::fs` spellings too.
4346 if path.rsplit("::").nth(1) == Some("OpenOptions") {
4347 return &[];
4348 }
4349 // Reads the source AND writes the destination in one call.
4350 if matches!(leaf, "copy" | "rename" | "hard_link" | "soft_link" | "symlink") {
4351 return &["read", "write"];
4352 }
4353 const WRITE: &[&str] = &[
4354 "write", "write_all", "write_fmt", "write_vectored", "create", "create_new", "create_dir",
4355 "create_dir_all", "remove_file", "remove_dir", "remove_dir_all", "set_permissions",
4356 "set_len", "set_modified", "set_times", "append", "flush", "sync_all", "sync_data",
4357 "write_at", "truncate",
4358 ];
4359 const READ: &[&str] = &[
4360 "read", "read_to_string", "read_to_end", "read_exact", "read_dir", "read_link",
4361 "metadata", "symlink_metadata", "exists", "try_exists", "canonicalize", "open",
4362 "read_at", "file_type", "permissions", "modified", "accessed", "created", "len",
4363 ];
4364 if WRITE.contains(&leaf) { return &["write"]; }
4365 if READ.contains(&leaf) { return &["read"]; }
4366 // `OpenOptions` carries the direction in its BUILDER chain, not its terminal verb, so `.open()` on one
4367 // says nothing here and is deliberately left to the READ arm above only when it is `File::open`
4368 // (unambiguously a read). Anything else: no claim.
4369 if leaf.starts_with("write") || leaf.starts_with("append") { return &["write"]; }
4370 if leaf.starts_with("read") { return &["read"]; }
4371 &[]
4372}
4373
4374pub fn classify_command_head(cmd: &str) -> &'static [&'static str] {
4375 // Only UNAMBIGUOUS single-effect tools belong here. A multi-modal head (`git status` is local,
4376 // `git push` is Net; `rsync` local-vs-remote) would FABRICATE the effect for its common case —
4377 // the under-report rule forbids it, so such heads keep the bare cliff.
4378 match cmd.rsplit(['/', '\\']).next().unwrap_or(cmd) {
4379 "curl" | "wget" | "http" | "ssh" | "scp" | "sftp" | "ftp" | "telnet" => &["Net"],
4380 "psql" | "mysql" | "sqlite3" | "mongosh" | "mongo" | "redis-cli" | "cqlsh" | "influx" => &["Db"],
4381 // candor engines — Fs/Env only, guaranteed by spec §7 item 12 (the analyzer self-boundary)
4382 "candor" | "candor-run.sh" | "candor-scan" | "candor-query" | "candor-java"
4383 | "candor-classify" | "candor-report" | "cargo-candor" => &["Env", "Fs"],
4384 _ => &[],
4385 }
4386}
4387
4388/// Known machine-learning MODEL-provider hosts — the SPEC §1 ⟨0.13⟩ `Llm` host-literal refinement:
4389/// a statically-known `Net` request to one of these classifies `Llm` IN ADDITION to `Net` (Net is
4390/// never dropped — a model call IS network I/O, exactly as an `Exec`-refined subprocess keeps `Exec`),
4391/// just as a jdbc URL classifies `Db`. Matched by host, case-insensitive; a SUBDOMAIN of a listed host
4392/// counts. The reference engines share this table VERBATIM with candor-java's `Literals.MODEL_HOSTS`
4393/// (the analog of `classify_command_head`) so the `Net` boundary refines to `Llm` identically. An
4394/// UNKNOWN host stays bare `Net` — never guessed. Curated STARTER set; the §7 coverage ledger
4395/// discloses an uncovered provider like any other.
4396pub const MODEL_HOSTS: &[&str] = &[
4397 "api.openai.com",
4398 "api.anthropic.com",
4399 "generativelanguage.googleapis.com",
4400 "api.mistral.ai",
4401 "api.cohere.ai",
4402 "api.cohere.com",
4403 "api.groq.com",
4404 "api.together.xyz",
4405 "api.perplexity.ai",
4406 "openrouter.ai",
4407];
4408
4409/// Whether an endpoint HOST literal is a known model provider (case-insensitive; a subdomain of a
4410/// `MODEL_HOSTS` entry counts). Strips a `:port` suffix first. Two special forms carry their own rule,
4411/// matching candor-java's `Literals.isModelHost` exactly: any host whose port is `11434` is a local
4412/// Ollama endpoint (a LOOPBACK host — `localhost`/`127.0.0.1`/`::1` — on port 11434); and an AWS Bedrock
4413/// runtime host (the model-inference service label `bedrock-runtime`/`bedrock-agent-runtime`).
4414pub fn is_model_host(host_literal: &str) -> bool {
4415 // Strip any `:port` (via the shared host_part) and lowercase for the name comparisons.
4416 let host = policy::host_part(host_literal).to_ascii_lowercase();
4417 // Ollama is a LOCAL endpoint: :11434 → Llm ONLY on a loopback host (max-review r3 parity fix — "any
4418 // host on :11434" fabricated Llm on unrelated internal services on that port).
4419 if let Some((_, port)) = host_literal.rsplit_once(':') {
4420 if port == "11434" {
4421 return matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1");
4422 }
4423 }
4424 if MODEL_HOSTS.contains(&host.as_str()) {
4425 return true;
4426 }
4427 // A subdomain of a known model host counts (`eu.api.openai.com` → api.openai.com).
4428 if MODEL_HOSTS.iter().any(|m| host.ends_with(&format!(".{m}"))) {
4429 return true;
4430 }
4431 // AWS Bedrock runtime: the FIRST label is the model-inference service (`bedrock-runtime.<region>.
4432 // amazonaws.com`), NOT the substring "bedrock" (which caught `bedrock-backups.s3.amazonaws.com`, an
4433 // S3 bucket) and NOT the control-plane `bedrock.<region>.amazonaws.com`.
4434 host.ends_with(".amazonaws.com")
4435 && matches!(host.split('.').next(), Some("bedrock-runtime") | Some("bedrock-agent-runtime"))
4436}
4437
4438/// ⟨0.20⟩ Curated telemetry / analytics / APM hosts — the `Net` destination-class `known-telemetry` set
4439/// (NET-DESTINATION-CLASS-DESIGN.md), shared VERBATIM with candor-java's `Literals.TELEMETRY_HOSTS` (like
4440/// `MODEL_HOSTS`). A benign observability endpoint. Matched by host, case-insensitive; a SUBDOMAIN of a
4441/// listed host counts. Tight, high-precision STARTER set — mis-including an exfil-capable host would
4442/// under-gate `deny Net[unknown-host]`.
4443pub const TELEMETRY_HOSTS: &[&str] = &[
4444 "sentry.io",
4445 "bugsnag.com",
4446 "rollbar.com",
4447 "segment.io",
4448 "segment.com",
4449 "mixpanel.com",
4450 "amplitude.com",
4451 "google-analytics.com",
4452 "analytics.google.com",
4453 "datadoghq.com",
4454 "datadoghq.eu",
4455 "newrelic.com",
4456 "nr-data.net",
4457 "honeycomb.io",
4458 "logtail.com",
4459 // ⟨0.20.1⟩ corpus-grown (a real-repo dogfood): more single-purpose analytics / session-replay / RUM
4460 // providers — vendor-specific product domains only (no general-purpose host), so no under-gate risk.
4461 "posthog.com",
4462 "plausible.io",
4463 "usefathom.com",
4464 "heapanalytics.com",
4465 "fullstory.com",
4466 "hotjar.com",
4467 "logrocket.com",
4468 "cloudflareinsights.com",
4469];
4470
4471/// Whether an endpoint HOST literal is in `set` (case-insensitive; a subdomain of a listed host counts).
4472/// Strips a `:port` suffix first via `host_part`. The shared membership test for `TELEMETRY_HOSTS` and the
4473/// config-declared partner set (mirrors candor-java's `Literals.hostInSet`).
4474pub fn host_in_set(host_literal: &str, set: &[&str]) -> bool {
4475 let host = policy::host_part(host_literal).to_ascii_lowercase();
4476 set.contains(&host.as_str()) || set.iter().any(|e| host.ends_with(&format!(".{e}")))
4477}
4478
4479/// Whether an endpoint HOST literal is a known telemetry/analytics/APM host (`TELEMETRY_HOSTS`).
4480pub fn is_telemetry_host(host_literal: &str) -> bool {
4481 host_in_set(host_literal, TELEMETRY_HOSTS)
4482}
4483
4484/// ⟨0.20⟩ The `Net` DESTINATION CLASS of a host literal (NET-DESTINATION-CLASS-DESIGN.md): `known-telemetry`
4485/// (curated), `known-partner` (config `net-partner` OR a model host — a declared-ish external API), else
4486/// `unknown-host` — the HONEST default (candor makes no claim; the security gate bites this). A partner set
4487/// is per-project (config-declared). Never fabricated onto a safe class: an unresolved host is unknown-host.
4488/// Mirrors candor-java's `Literals.netDestClass`.
4489pub fn net_dest_class(host_literal: &str, partners: &std::collections::BTreeSet<String>) -> &'static str {
4490 if is_telemetry_host(host_literal) {
4491 return "known-telemetry";
4492 }
4493 if partner_for(host_literal, partners).is_some() || is_model_host(host_literal) {
4494 return "known-partner";
4495 }
4496 "unknown-host"
4497}
4498
4499/// ⟨0.31⟩ WHICH declared partner a host matched, or `None` — the SAME match `net_dest_class` decides on,
4500/// extracted so the DISCLOSURE and the DECISION cannot use different rules.
4501///
4502/// That is not a stylistic preference. The reverted first attempt at the `net-partner` disclosure
4503/// re-implemented this match against a normaliser that KEEPS the port, so an observed
4504/// `partner.example:443` never equalled a declared `partner.example` and the disclosure came back
4505/// SILENTLY EMPTY on every real run — while the verdicts it was reporting on had flipped. A disclosure
4506/// normalised differently from the decision it reports can only be wrong, and one function with two
4507/// callers is what makes writing one impossible.
4508pub fn partner_for(
4509 host_literal: &str,
4510 partners: &std::collections::BTreeSet<String>,
4511) -> Option<String> {
4512 if partners.is_empty() {
4513 return None;
4514 }
4515 let host = policy::host_part(host_literal).to_ascii_lowercase();
4516 if partners.contains(&host) {
4517 return Some(host);
4518 }
4519 partners
4520 .iter()
4521 .find(|p| host.ends_with(&format!(".{p}")))
4522 .cloned()
4523}
4524
4525/// ⟨0.20⟩ The closed `Net` destination-class vocabulary, for the `deny Net[<dest…>]` policy filter.
4526pub const NET_DEST_CLASSES: &[&str] = &["known-telemetry", "known-partner", "unknown-host"];
4527
4528/// Curated Rust model-provider SDK crates — the SPEC §1 ⟨0.13⟩ `Llm` model-SDK surface, the Rust analog
4529/// of candor-java's `Rules.MODEL_SDK_PACKAGES`. A resolved call into one of these crates classifies
4530/// `Llm` + `Net` (the caller adds both — a model dispatch IS network I/O). NO method-name gating: these
4531/// are single-purpose provider clients, so ANY call into the crate is a model dispatch (matches the java
4532/// reference's judgment call). Curated STARTER list; the §7 coverage ledger discloses the rest.
4533pub const MODEL_SDK_CRATES: &[&str] = &[
4534 "async_openai", // async-openai — the de-facto OpenAI client
4535 "anthropic_sdk", // anthropic-sdk
4536 "anthropic", // anthropic (community client crate)
4537 "aws_sdk_bedrockruntime", // AWS Bedrock runtime (invoke/converse) — the model surface of the aws-sdk family
4538 "ollama_rs", // ollama-rs — local Ollama client
4539 "langchain_rust", // langchain-rust — the LangChain invoke surfaces
4540 "mistralai", // mistralai (Mistral client)
4541 "genai", // genai — a multi-provider model client
4542];
4543
4544/// Whether a resolved call's CRATE is a curated model-provider SDK (`MODEL_SDK_CRATES`) → the SPEC §1
4545/// ⟨0.13⟩ `Llm` model-SDK classification (the caller adds both `Llm` and `Net`). Crate-level, no
4546/// method gating — a single-purpose client, matching candor-java's `isModelSdkOwner`.
4547pub fn is_model_sdk_crate(crate_name: &str) -> bool {
4548 MODEL_SDK_CRATES.contains(&crate_name)
4549}
4550
4551/// The libc / nix / rustix PATH-and-directory syscall leaves `classify` charges `Fs` (incl. `*64` and
4552/// `*at` variants). Module-level, not local to `classify`, because it is ONE of the two answers to one
4553/// question: the AS-EFF-008 masking guard (`masks_locator`) reads this same table, so a leaf added here
4554/// is masked by default — see `LIBC_FS_NO_PATH` for the direction. SOUNDNESS R806.
4555pub const LIBC_FS: &[&str] = &[
4556 "open", "open64", "openat", "openat2", "creat", "creat64", "stat", "stat64", "lstat",
4557 "lstat64", "fstatat", "fstatat64", "newfstatat", "statx", "access", "faccessat",
4558 "faccessat2", "mkdir", "mkdirat", "rmdir", "unlink", "unlinkat", "rename", "renameat",
4559 "renameat2", "link", "linkat", "symlink", "symlinkat", "readlink", "readlinkat", "chmod",
4560 "fchmodat", "chown", "lchown", "fchownat", "truncate", "truncate64", "ftruncate",
4561 "ftruncate64", "opendir", "fdopendir", "readdir", "readdir64", "readdir_r", "closedir",
4562 "rewinddir", "seekdir", "telldir", "scandir", "mkstemp", "mkstemps", "mkostemp", "mkdtemp",
4563 "mknod", "mknodat", "chdir", "fchdir", "getcwd", "get_current_dir_name", "chroot",
4564 "pivot_root", "statfs", "statfs64", "fstatfs", "fstatfs64", "statvfs", "fstatvfs", "mount",
4565 "umount", "umount2", "fsync", "fdatasync", "sync", "syncfs", "sync_file_range", "fallocate",
4566 "posix_fallocate", "posix_fadvise", "sendfile", "sendfile64", "copy_file_range", "flock",
4567 "getdents", "getdents64", "utime", "utimes", "lutimes", "futimens", "utimensat", "futimesat",
4568 "realpath",
4569 ];
4570
4571/// The libc / nix / rustix socket-family leaves `classify` charges `Net`. See `LIBC_NET_NO_DESTINATION`.
4572pub const LIBC_NET: &[&str] = &[
4573 "socket", "setsockopt", "getsockopt", "bind", "listen", "accept", "accept4", "connect",
4574 "shutdown", "send", "sendto", "sendmsg", "sendmmsg", "recv", "recvfrom", "recvmsg",
4575 "recvmmsg", "getpeername", "getsockname", "getaddrinfo", "freeaddrinfo", "getnameinfo",
4576 ];
4577
4578/// The libc / nix / rustix process creation / replacement / reaping leaves `classify` charges `Exec`.
4579/// See `LIBC_EXEC_NO_PROGRAM`.
4580pub const LIBC_EXEC: &[&str] = &[
4581 "fork", "vfork", "clone", "clone3", "execl", "execlp", "execle", "execv", "execvp",
4582 "execvpe", "execve", "execveat", "fexecve", "posix_spawn", "posix_spawnp", "system",
4583 "popen", "pclose", "wait", "waitpid", "wait3", "wait4", "waitid",
4584 ];
4585
4586/// The Postgres / SQLite-family client verbs `classify` charges `Db` for `postgres`, `tokio_postgres`,
4587/// `deadpool_postgres` and `rusqlite` — the round-trips themselves. Module-level because the masking
4588/// guard reads it too (`masks_locator`, SOUNDNESS R806): every verb here takes the SQL text at
4589/// argument 0 except the three in `PG_DB_VERBS_NO_SQL`, so a verb added here is masked by default.
4590/// `(type, method)` of a `crate::[modules::]Type::method` path whose modules are exactly one of `mods`
4591/// (the crate-root re-export when `mods` holds `""`). Used by the coverage-gate rules below that name a
4592/// method on ONE type: comparing the type SEGMENT, not a suffix, so `MultiplexedConnection::recv_response`
4593/// can never satisfy a rule written for `Connection::recv_response`.
4594/// SOUNDNESS R1607 — the sea_orm `ActiveModelTrait` / `ModelTrait` members that run a statement: the provided `insert`,
4595/// `update`, `update_without_returning`, `save`, `delete` and the link-table writers (`entity/active_model.rs`, async, each
4596/// through a connection; `ModelTrait::delete`, `entity/model.rs`). Read from sea-orm 2.0.4 and EXECUTED for `insert`
4597/// (2 statements, INSERT and the read-back) and `ModelTrait::delete` (1). The rule was `path.contains("ActiveModelTrait::")`,
4598/// which also charged the pure accessors (`get`, `set`, `take`, `is_changed`, `from_json`, `find_related` — a builder);
4599/// those are named only now that a dot-call on a derive-generated `ActiveModel` reaches the trait's spelling (`scan.rs`
4600/// R1607), so the rule names the member.
4601fn r1607_active_model_query(path: &str) -> bool {
4602 let Some((head, m)) = path.rsplit_once("::") else { return false };
4603 if head.ends_with("::ActiveModelTrait") || head == "ActiveModelTrait" {
4604 return matches!(
4605 m,
4606 "insert" | "update" | "update_without_returning" | "save" | "delete" | "establish_links" | "establish_links_self"
4607 | "establish_links_self_rev" | "delete_links" | "delete_links_self"
4608 );
4609 }
4610 (head.ends_with("::ModelTrait") || head == "ModelTrait") && m == "delete"
4611}
4612
4613fn type_method<'a>(path: &'a str, krate: &str, mods: &[&str]) -> Option<(&'a str, &'a str)> {
4614 let rest = path.strip_prefix(krate)?.strip_prefix("::")?;
4615 let (head, method) = rest.rsplit_once("::")?;
4616 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4617 mods.contains(&module).then_some((ty, method))
4618}
4619
4620/// SOUNDNESS R1528 — isahc 2.0.1 (ceiling 2.0.1, resolved 2.0.1: in-ceiling, so a `Cargo.lock` consumer and a lockless
4621/// one both got NOTHING for these rows). EXECUTED in a consumer fixture against a local HTTP server that counts requests,
4622/// beside an interposer logging `connect`, `getenv` and `clock_gettime` between a start and an end marker (handoff
4623/// `rust-r1528/mq/`).
4624///
4625/// CHARGED `Net`: `HttpClient::{get,head,post,put,delete,send}_async` and `Request::send_async` (the `RequestExt` method)
4626/// each put ONE request on the wire (EXECUTED: the server counted exactly one request and the process connected to it for
4627/// every one of the seven; the sync twins already classify through `::get`/`::send`). The existing rule only named the
4628/// FREE functions `isahc::get_async` / `isahc::send_async`; the comment beside it spoke of `HttpClient::shared()`, which
4629/// is `pub(crate)` and cannot be called at all, so the method spelling was never reachable by a rule.
4630/// CHARGED `Env`: `TrustStore::native` reads `SSL_CERT_FILE` (EXECUTED: the `getenv` is logged inside the window and the
4631/// control logs none). It is a `LazyLock`, so the read happens on the FIRST call in a process only: a second call in the
4632/// same process reads nothing. That over-charge is accepted, because the first call is the one every program makes and
4633/// the alternative is a silence on it.
4634///
4635/// NOT charged, and filed (R1592): `CookieJar::{get_by_name, get_for_uri, set}` read the clock (`SystemTime::now`)
4636/// only when the stored cookie carries an `expiration` (`Cookie::is_expired`): EXECUTED 1 `clock_gettime` with an
4637/// expiring cookie and 0 with an empty jar and with a session cookie, for all three. A `Clock` rule would charge two of the
4638/// three shapes of jar. `Response::text` blocks on the response body: EXECUTED returning the tail the server sent 1.5 s
4639/// after the headers, and reading nothing from a response built in memory.
4640fn isahc_r1528(path: &str) -> Option<&'static str> {
4641 let rest = path.strip_prefix("isahc::")?;
4642 let (head, method) = rest.rsplit_once("::")?;
4643 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4644 match (module, ty, method) {
4645 ("" | "client", "HttpClient", "get_async" | "head_async" | "post_async" | "put_async" | "delete_async" | "send_async")
4646 | ("" | "request", "Request", "send_async") => Some("Net"),
4647 ("" | "tls" | "tls::trust", "TrustStore", "native") => Some("Env"),
4648 _ => None,
4649 }
4650}
4651
4652/// SOUNDNESS R1528 — lettre 0.11.23 (ceiling 0.11.23, resolved 0.11.23: in-ceiling, both arms got nothing). EXECUTED in a
4653/// consumer fixture under an interposer (`getenv`, `open`, `connect`, `clock_gettime`, entropy between a start and an end
4654/// marker) beside an SMTP sink (docker `axllent/mailpit` behind a counting TCP proxy), handoff `rust-r1528/mq/`.
4655///
4656/// The scan attributes every chain hop off `Message::builder()` / `MultiPart::builder()` to the ROOT type
4657/// (`Message::builder().from(..).to(..).body(..)` reaches classify as `lettre::message::Message::body`), so each rule below
4658/// is keyed on BOTH spellings, `MessageBuilder::<m>` (a typed receiver, the grandfathered rows) and `Message::<m>` (the
4659/// chain). `Message` has no method of those names, so the second spelling cannot fabricate.
4660///
4661/// CHARGED `Clock`: `Date::now`, `MessageBuilder::date_now`, `Message::sign` and `dkim::dkim_sign` read `SystemTime::now`
4662/// (EXECUTED 1 `clock_gettime` each, 0 for the control), and so do `body`, `singlepart` and `multipart` whenever the builder
4663/// holds no `Date` header (`build` inserts `date_now()`, mod.rs:418): EXECUTED 1 read, and 0 once `.date(ts)` was set. The
4664/// over-charge on `.date(ts).body(..)` is accepted: the common idiom sets no date.
4665/// CHARGED `Rand`: `MultiPartBuilder::kind` (and its constructors `MultiPart::{mixed, alternative, related, signed,
4666/// encrypted, alternative_plain_html}`, which are `builder().kind(..)` and were never rows: the generator's self-scan does
4667/// not resolve that call) draw a boundary with `fastrand` (EXECUTED: two processes printed different 40-character
4668/// boundaries; `mixed` and `kind` executed, the other constructors read), and `MessageBuilder::message_id` does when
4669/// given `None` (EXECUTED: two processes printed different ids, and `Some("<x@y>")` printed the same id twice, so
4670/// `message_id(Some(..))` is an accepted over-charge). `fastrand` seeds from the time and the thread id, not from OS
4671/// entropy (0 entropy reads): the evidence is non-determinism across processes, which is weaker than an entropy interposer.
4672/// CHARGED `Fs`: `AsyncFileTransport::read` (EXECUTED: the open of `<dir>/<id>.eml` is logged) and the executor's
4673/// `fs_read` / `fs_write` (`Executor`, `Tokio1Executor`, `AsyncStd1Executor`; EXECUTED for `Tokio1Executor`: the read
4674/// and the write opened and created their files; the async-std twin is READ only).
4675///
4676/// READ PURE, `REVIEWED_PURE_ENTRIES`: `SmtpTransport::unencrypted_localhost` and `AsyncSmtpTransport::
4677/// unencrypted_localhost` take no argument, so the pool is always the default (`min_idle` 0): the maintenance thread / task
4678/// has nothing to connect (EXECUTED held 1.5 s: no `connect`, 0 connections through the proxy, sync and async).
4679///
4680/// NOT charged, and filed (R1592): `SmtpTransportBuilder::build` and `AsyncSmtpTransportBuilder::build` connect from the
4681/// background pool only when the caller asked for `PoolConfig::min_idle(n > 0)` (EXECUTED: 0 connections by default, 1
4682/// connection and 23 bytes up with `min_idle(1)`, sync and async). `Auth::new_from_response` emits two `debug!` records
4683/// only under lettre's `tracing` feature, which is off by default (EXECUTED with the feature: two records; the feature is
4684/// the caller's choice, so a `Log` rule would charge every default build).
4685///
4686/// One effect is all `classify` can carry: `message_id(None)` also reads the host name, and `body` on a dateless builder
4687/// also formats the headers; only the effect named is charged (R1575).
4688fn lettre_r1528(path: &str) -> Option<&'static str> {
4689 let rest = path.strip_prefix("lettre::")?;
4690 if matches!(rest, "message::dkim::dkim_sign" | "message::dkim_sign" | "dkim_sign") {
4691 return Some("Clock");
4692 }
4693 let (head, method) = rest.rsplit_once("::")?;
4694 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4695 match (module, ty, method) {
4696 ("" | "message", "MessageBuilder" | "Message", "body" | "singlepart" | "multipart" | "date_now") => Some("Clock"),
4697 ("" | "message", "Message", "sign") => Some("Clock"),
4698 ("" | "message::header" | "message::header::date", "Date", "now") => Some("Clock"),
4699 ("" | "message", "MessageBuilder" | "Message", "message_id") => Some("Rand"),
4700 ("" | "message" | "message::mimebody", "MultiPartBuilder" | "MultiPart", "kind") => Some("Rand"),
4701 (
4702 "" | "message" | "message::mimebody",
4703 "MultiPart",
4704 "mixed" | "alternative" | "related" | "signed" | "encrypted" | "alternative_plain_html",
4705 ) => Some("Rand"),
4706 ("" | "transport::file", "AsyncFileTransport", "read") => Some("Fs"),
4707 ("" | "executor", "Executor" | "Tokio1Executor" | "AsyncStd1Executor", "fs_read" | "fs_write") => Some("Fs"),
4708 _ => None,
4709 }
4710}
4711
4712/// SOUNDNESS R1604 — a `get` that reads a header map, charged `Net` by the URL-naming `::get` suffix rule. The scan attributes
4713/// a method chain to its ROOT type, so `resp.headers().get("x")` reaches classify as `reqwest::Response::get`, and a typed
4714/// `HeaderMap` receiver as `reqwest::header::HeaderMap::get`. None of these types has a `get` of its own in reqwest 0.11, 0.12
4715/// or 0.13 (READ: the only `get`s are `Client::get`, `blocking::Client::get`, the wasm client and the two free fns), so the
4716/// hop is always `http::HeaderMap::get` or `http::Extensions::get`, an in-memory lookup. EXECUTED over a local HTTP server
4717/// with a connect/send/recv interposer (handoff `rust-fab/`): 0 socket calls for the typed map, the response and request
4718/// header chains and the blocking response; every twin (`Client::get(..).send()`, `head(..).send()`, blocking, `reqwest::get`)
4719/// connects and is unchanged. A hop that does read the body (`resp.json().await?.get(..)`) is charged at `json`, not here.
4720///
4721/// isahc: its `Response` is `http::Response`, whose header chain is the same lookup (EXECUTED 0 socket calls). `isahc::Request::
4722/// {get,post,..}` is deliberately NOT here: in the crate's own idiom `Request::post(uri).body(b)?.send()?` the `.send()` hop is
4723/// untyped and reaches no rule, so the URL-naming constructor is the only `Net` on code that really sends (EXECUTED: connect
4724/// and a request at the server). Removing it would make `deny Net` pass over that code.
4725fn http_header_get_r1604(path: &str) -> bool {
4726 matches!(
4727 path,
4728 "reqwest::header::HeaderMap::get"
4729 | "reqwest::Response::get"
4730 | "reqwest::blocking::Response::get"
4731 | "reqwest::Request::get"
4732 | "reqwest::blocking::Request::get"
4733 | "isahc::Response::get"
4734 | "isahc::http::Response::get"
4735 | "isahc::http::response::Response::get"
4736 | "isahc::http::HeaderMap::get"
4737 | "isahc::http::header::HeaderMap::get"
4738 )
4739}
4740
4741/// SOUNDNESS R1604 — lettre 0.11.23 `StubTransport` / `AsyncStubTransport` `send` / `send_raw` push the message onto an in-memory
4742/// `Mutex<Vec<..>>` and return the configured result (READ: transport/stub/mod.rs:156,171; the trait's provided `send` formats
4743/// the message into a `Vec`). EXECUTED: 0 connects, 0 file creates, 0 processes, the stub's log grows by one. The `::send` suffix
4744/// rule charged it `Net`. Keyed on module + type; the SMTP transports and the generic `Transport::send` keep `Net` (EXECUTED: a
4745/// connect and one delivered message at a local SMTP responder), File and Sendmail keep `Fs` / `Exec` (R1597).
4746fn lettre_stub_send_r1604(path: &str) -> bool {
4747 matches!(
4748 path,
4749 "lettre::transport::stub::StubTransport::send"
4750 | "lettre::transport::stub::StubTransport::send_raw"
4751 | "lettre::transport::stub::AsyncStubTransport::send"
4752 | "lettre::transport::stub::AsyncStubTransport::send_raw"
4753 )
4754}
4755
4756/// SOUNDNESS R1597 — lettre 0.11.23 `Transport::send` / `send_raw` on the transports that do not speak SMTP. The `::send`
4757/// suffix rule charges `Net` for every transport, which is wrong in both directions for these types, EXECUTED in a consumer
4758/// fixture beside an interposer (`open`, `connect`) and a stand-in `sendmail` script that logs each invocation (handoff
4759/// `rust-r1528/b10/`):
4760/// * `FileTransport` / `AsyncFileTransport` write `<dir>/<id>.eml` (EXECUTED: the `open(..., O_CREAT|O_WRONLY)` is logged,
4761/// one file per send, sync and async, `send` and `send_raw`; 0 `connect`). Real effect `Fs`: `deny Fs` passed over code
4762/// that writes an email to disk, and `deny Net` failed over it.
4763/// * `SendmailTransport` / `AsyncSendmailTransport` spawn the configured `sendmail` command and pipe the message to it
4764/// (EXECUTED: exactly one invocation of the stand-in per send, 0 `connect`). Real effect `Exec`.
4765///
4766/// `SmtpTransport` / `AsyncSmtpTransport` stay `Net` (EXECUTED: a `connect` to the SMTP port; the generic `Transport::send`
4767/// on a type parameter keeps the suffix rule's `Net`, since SMTP is the idiom behind it). `StubTransport` does nothing
4768/// (EXECUTED: 0 files, 0 connects, 0 processes) and is carved out separately (R1604, `lettre_stub_send_r1604`). Keyed on module + type: `AsyncFileTransport<E>` reaches classify without its type argument.
4769fn lettre_send_r1597(path: &str) -> Option<&'static str> {
4770 let rest = path.strip_prefix("lettre::")?;
4771 let (head, method) = rest.rsplit_once("::")?;
4772 if !matches!(method, "send" | "send_raw") {
4773 return None;
4774 }
4775 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4776 match (module, ty) {
4777 ("" | "transport::file", "FileTransport" | "AsyncFileTransport") => Some("Fs"),
4778 ("" | "transport::sendmail", "SendmailTransport" | "AsyncSendmailTransport") => Some("Exec"),
4779 _ => None,
4780 }
4781}
4782
4783/// SOUNDNESS R1528 (b10) — tempfile 3.27.0 (ceiling 3.27.0, resolved 3.27.0: in-ceiling, so a `Cargo.lock` consumer and a
4784/// lockless one both got NOTHING for these rows). EXECUTED in a consumer fixture under a DYLD interposer that logs `getenv`,
4785/// `open` (with O_CREAT), `unlink` and `getcwd` between a start and an end marker (handoff `rust-r1528/b10/`).
4786///
4787/// CHARGED `Fs`: `SpooledTempFile::{into_file, roll}` on an in-memory buffer create a real temporary file (EXECUTED: one
4788/// `open(<TMPDIR>/.tmpXXXXXX, O_CREAT|O_WRONLY)` and one `unlink`, after a `TMPDIR` read) and write the buffer into it. The
4789/// accepted over-charge is a receiver that already spilled past `max_size`: EXECUTED 0 events for both calls on a rolled one.
4790/// The alternative is a silence on the common idiom (`write` a little, then `into_file`).
4791/// CHARGED `Env`: `tempfile::env::temp_dir` reads `TMPDIR` (EXECUTED: `getenv TMPDIR`), unless `override_temp_dir` was called
4792/// first (EXECUTED: 0 reads) — an accepted over-charge, the default idiom is the one that reads.
4793///
4794/// CARVED OUT of the `::keep` suffix rule: `TempDir::keep` (and its deprecated `into_path`) only flips `disable_cleanup` and
4795/// moves the path out (dir/mod.rs:418, READ; EXECUTED: 0 `open`, 0 `unlink`, 0 `mkdir`, 0 `getenv` for both). `NamedTempFile::keep`
4796/// and `TempPath::keep` are NOT carved out: they call `imp::keep`, which is a no-op on unix and a real file-attribute change
4797/// on Windows, which this lane could not execute.
4798///
4799/// NOT charged, and filed (R1595): `TempPath::{from_path, try_from_path}` read the working directory only for a RELATIVE path
4800/// (EXECUTED: 1 `getcwd` for `"rel/x"`, 0 for an absolute path and for the empty path); they touch no file.
4801fn tempfile_r1528(path: &str) -> Option<&'static str> {
4802 let rest = path.strip_prefix("tempfile::")?;
4803 if matches!(rest, "temp_dir" | "env::temp_dir") {
4804 return Some("Env");
4805 }
4806 let (head, method) = rest.rsplit_once("::")?;
4807 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4808 match (module, ty, method) {
4809 ("" | "spooled", "SpooledTempFile", "into_file" | "roll") => Some("Fs"),
4810 _ => None,
4811 }
4812}
4813
4814/// SOUNDNESS R1528 (b10) — tungstenite 0.30.0 (ceiling 0.30.0, resolved 0.30.0: in-ceiling, both arms got NOTHING for these
4815/// rows). EXECUTED against a local hand-written WebSocket server (counting connections and received bytes) beside a DYLD
4816/// interposer logging `setsockopt` and entropy reads, each between start and end markers (handoff `rust-r1528/b10/`).
4817///
4818/// CHARGED `Rand`: `handshake::client::generate_key` and the `IntoClientRequest` impls that call it, `Uri` and
4819/// `ClientRequestBuilder` (the scan spells the typed `Uri` receiver `tungstenite::http::Uri::into_client_request`): EXECUTED
4820/// 1 OS entropy read on a fresh thread for each, 0 for `Request::into_client_request`, which is a pass-through and stays
4821/// pure (it would be charged by a `::into_client_request` suffix, so the rule keys on the type). The `&str`/`String`
4822/// impls forward to `Uri`; a `&str` receiver is untyped for the scan and cannot be reached by any rule.
4823/// CHARGED `Net`: `NoDelay::set_nodelay` on `MaybeTlsStream` and on a `TcpStream` (EXECUTED: `setsockopt(IPPROTO_TCP,
4824/// TCP_NODELAY)` on the socket; no byte moves, the std rule already charges every `TcpStream` method `Net`), and
4825/// `WebSocket::write_pending`, the deprecated alias of `flush` (EXECUTED: 0 bytes at the server with nothing buffered, 19
4826/// bytes — a masked text frame — with one buffered; the existing `::flush` rule charges the same call `Net`).
4827fn tungstenite_r1528(path: &str) -> Option<&'static str> {
4828 let rest = path.strip_prefix("tungstenite::")?;
4829 if matches!(rest, "handshake::client::generate_key" | "client::generate_key" | "generate_key") {
4830 return Some("Rand");
4831 }
4832 let (head, method) = rest.rsplit_once("::")?;
4833 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4834 match (module, ty, method) {
4835 ("" | "http" | "client" | "http::uri", "Uri", "into_client_request") => Some("Rand"),
4836 ("" | "client", "ClientRequestBuilder", "into_client_request") => Some("Rand"),
4837 ("" | "stream", "MaybeTlsStream" | "TcpStream" | "NoDelay", "set_nodelay") => Some("Net"),
4838 ("" | "protocol", "WebSocket", "write_pending") => Some("Net"),
4839 _ => None,
4840 }
4841}
4842
4843/// SOUNDNESS R1528 (b11) — the last 51 grandfathered rows, at the versions the manifests were generated from. EXECUTED in
4844/// a consumer fixture under a DYLD interposer (`getenv`, `setenv`, `open`, `stat`, `connect`, entropy, `clock_gettime`,
4845/// `fork`/`spawn`) and, where a service is the point, against PostgreSQL 16 behind a counting TCP proxy; the two
4846/// Linux-only questions (the openssl trust store) under `strace` in `rust:1-bookworm` (handoff `rust-r1528/b11/`).
4847/// `CALIBRATED_CEILINGS` is NOT raised for console (0.16.6 vs 0.15.11), rand (0.10.3 vs 0.10.2) or rustls (0.23.45 vs
4848/// 0.23.43): that needs the whole crate table re-verified at the new version.
4849///
4850/// Returns `Some(Some(effect))` for a rule, `Some(None)` for a proven-pure carve-out of a rule that fires later in
4851/// `classify`, and `None` to fall through. `classify` carries the PRIMARY effect; where a row does two things the second is
4852/// an `ALSO_EFFECTS` row (R1612, closing R1599's four): `next_backoff` draws entropy (Rand) AND reads the clock (Clock),
4853/// `try_associate` reads `PATH` (Env) AND stats each directory (Fs), `UtcOffset::current_local_offset` reads the clock
4854/// (Clock) AND the zone database (Fs), and the elasticsearch transport reads the proxy variables (Env) AND, on
4855/// the openssl backend, the system trust store (Fs, carried by `ALSO_EFFECTS` as the second effect).
4856///
4857/// CHARGED `Clock`: `env_logger` `Formatter::timestamp{,_seconds,_millis,_micros,_nanos}` (each reads `SystemTime::now`) and
4858/// `ConfigurableFormat::format` (EXECUTED: 1 read by default, 0 after `.timestamp(None)` — accepted over-charge);
4859/// elasticsearch `MultiNodeConnectionPool::reseed`; backoff `ExponentialBackoff::{get_elapsed_time, reset, default}`,
4860/// `ExponentialBackoffBuilder::build`, `SystemClock::now` (the clock is `C`, the default `SystemClock` reads it: a test
4861/// clock is an accepted over-charge); time `Instant::elapsed`, `{Signed}Duration::time_fn`, `UtcOffset::current_local_offset`;
4862/// notify `PollWatcher::with_initial_scan` (its poll thread reads the clock on every pass; no path is opened until
4863/// `watch`); duct `Handle::{wait_timeout, wait_deadline}` (EXECUTED 7 reads in the wait loop; `wait` and `try_wait` read none).
4864/// CHARGED `Env`: elasticsearch `Transport::{single_node, static_node_list, sniffing_node_list, cloud}` and
4865/// `TransportBuilder::build` (reqwest's `ClientBuilder::build`: 9 `getenv`, the proxy variables, on every call; no request
4866/// reaches the server); grep_cli `DecompressionMatcherBuilder::{associate, try_associate}` (`PATH`, for a relative program;
4867/// an absolute path is an accepted over-charge); etcetera `{Apple, Xdg, Unix, Windows}::new` (`HOME`, every spelling);
4868/// console `is_dumb` (`TERM`) and `{set_,}{true_,}colors_enabled{,_stderr}` (first call: `CLICOLOR_FORCE` or the terminal
4869/// variables; a second call reads none — accepted); portable_pty `CommandBuilder::{new, from_argv, new_default_prog}` (the
4870/// builder snapshots the whole process environment: EXECUTED, `FX_PROBE` is visible in the builder when set and absent when
4871/// not; NO process is spawned, 0 `fork`/`posix_spawn`/`execve`, so `from_argv` and `new_default_prog` — which the `Exec`
4872/// fall-through below had been charging — are carved out of it); dotenvy `Iter::load_override` (`setenv` per variable).
4873/// CHARGED `Rand`: backoff `ExponentialBackoff::next_backoff`; rand `make_rng` (the lazily seeded thread RNG: 1 entropy read
4874/// on a fresh thread, 0 on the second call — accepted); rustls `ClientConnection::{new, new_with_alpn}`,
4875/// `UnbufferedClientConnection::{new, new_with_alpn}` and the QUIC twins (EXECUTED: 3 to 4 entropy reads each, the
4876/// client random and the key share — the effect the rows' recorded `Fs` was NOT: the key log opens only for a
4877/// `KeyLogFile`, and only after the server hello, EXECUTED 0 bytes in the file at `new`).
4878/// CHARGED `Db`: tokio_postgres (and deadpool_postgres's `Client`/`Transaction`/`ClientWrapper`, EXECUTED) `Client`/`Transaction` `{execute_raw, execute_typed, query_typed_raw, query_typed_one,
4879/// query_typed_opt, query_scalar, query_one_scalar, query_opt_scalar, simple_query_raw, check_connection, bind, bind_raw,
4880/// query_portal, query_portal_raw, commit, rollback, savepoint}` — NONE was matched by the verb list (`PG_DB_VERBS`);
4881/// EXECUTED against PostgreSQL 16 behind a counting proxy: every one sends bytes (idle control 0/0/0).
4882/// CHARGED `Fs`: native_tls `TlsConnectorBuilder::build` (EXECUTED on Linux/openssl under `strace`: `openssl.cnf`,
4883/// `cert.pem`, `certs/`, `ca-certificates.crt`; 0 events on macOS — platform-conditional, like the `TlsConnector::new`
4884/// rule beside it).
4885///
4886/// NOT charged, and filed (R1598, `conditional`): `deadpool_postgres::Config::{get_pg_config, builder, create_pool}` read
4887/// `USER` only when neither the config nor the url names one (EXECUTED 1 read vs 0); grep_cli `DecompressionMatcher::new`
4888/// and `resolve_binary` read `PATH` only under `cfg!(windows)` (EXECUTED 0 events on macOS), `is_readable_stdin` is an
4889/// `fstat` plus a `log::debug!` that emits only under a debug-level logger; tokio_postgres `Connection::poll_message` drives
4890/// the caller's stream (EXECUTED 65 bytes up over TCP, 0 over an in-memory `duplex`, 0 with nothing in flight); rand
4891/// `Fill::fill_slice` draws from the caller's `R` (EXECUTED 0 entropy for a seeded `StdRng`, 1 for the thread RNG).
4892fn b11_r1528(crate_name: &str, path: &str) -> Option<Option<&'static str>> {
4893 let rest = path.strip_prefix(crate_name)?.strip_prefix("::")?;
4894 // free functions first: the module/type split below would read `term::is_dumb` as a type
4895 match (crate_name, rest) {
4896 ("console", "is_dumb" | "term::is_dumb") => return Some(Some("Env")),
4897 ("console", "set_colors_enabled" | "utils::set_colors_enabled" | "set_colors_enabled_stderr"
4898 | "utils::set_colors_enabled_stderr" | "set_true_colors_enabled" | "utils::set_true_colors_enabled"
4899 | "set_true_colors_enabled_stderr" | "utils::set_true_colors_enabled_stderr" | "true_colors_enabled"
4900 | "utils::true_colors_enabled" | "true_colors_enabled_stderr" | "utils::true_colors_enabled_stderr") => {
4901 return Some(Some("Env"))
4902 }
4903 ("rand", "make_rng") => return Some(Some("Rand")),
4904 _ => {}
4905 }
4906 let (head, method) = rest.rsplit_once("::")?;
4907 let ty = head.rsplit("::").next().unwrap_or(head);
4908 let hit = |e: &'static str| Some(Some(e));
4909 match (crate_name, ty, method) {
4910 ("env_logger", "Formatter", "timestamp" | "timestamp_seconds" | "timestamp_millis" | "timestamp_micros" | "timestamp_nanos") => hit("Clock"),
4911 ("env_logger", "ConfigurableFormat", "format") => hit("Clock"),
4912 ("elasticsearch", "Transport", "single_node" | "static_node_list" | "sniffing_node_list" | "cloud") => hit("Env"),
4913 ("elasticsearch", "TransportBuilder", "build") => hit("Env"),
4914 ("elasticsearch", "MultiNodeConnectionPool", "reseed") => hit("Clock"),
4915 ("tokio_postgres" | "deadpool_postgres" | "postgres", "Client" | "Transaction" | "ClientWrapper", "execute_raw" | "execute_typed"
4916 | "query_typed_raw" | "query_typed_one" | "query_typed_opt" | "query_scalar" | "query_one_scalar"
4917 | "query_opt_scalar" | "bind" | "bind_raw" | "query_portal" | "query_portal_raw" | "commit" | "rollback"
4918 | "savepoint") => hit("Db"),
4919 ("tokio_postgres" | "deadpool_postgres" | "postgres", "Client" | "ClientWrapper", "simple_query_raw" | "check_connection") => hit("Db"),
4920 // deadpool's sealed `GenericClient` (implemented for its `Client` and `Transaction` only, both EXECUTED above)
4921 ("deadpool_postgres", "GenericClient", "execute_raw") => hit("Db"),
4922 // `ToStatementType` lives in tokio-postgres's private `to_statement::private` module and the method needs the
4923 // crate-private `Arc<InnerClient>`, so no consumer can call it; it is `prepare::prepare` (a Parse round trip), and
4924 // that is what is charged should a spelling ever reach it.
4925 ("tokio_postgres", "ToStatementType", "into_statement") => hit("Db"),
4926 // `DecompressionMatcherBuilder::build` is a glob-set compile plus `resolve_binary` (a no-op off Windows): it spawns
4927 // nothing, and the `::build` suffix rule for `CommandReaderBuilder::build` was charging it `Exec`. EXECUTED 0 spawns.
4928 ("grep_cli", "DecompressionMatcherBuilder", "build") => Some(None),
4929 ("grep_cli", "DecompressionMatcherBuilder", "associate" | "try_associate") => hit("Env"),
4930 ("backoff", "ExponentialBackoff", "get_elapsed_time" | "reset" | "default") => hit("Clock"),
4931 ("backoff", "ExponentialBackoff", "next_backoff") => hit("Rand"),
4932 ("backoff", "ExponentialBackoffBuilder", "build") => hit("Clock"),
4933 ("backoff", "SystemClock", "now") => hit("Clock"),
4934 ("rustls", "ClientConnection" | "UnbufferedClientConnection", "new" | "new_with_alpn") => hit("Rand"),
4935 ("etcetera", "Apple" | "Xdg" | "Unix" | "Windows", "new") => hit("Env"),
4936 ("time", "Instant", "elapsed") => hit("Clock"),
4937 ("time", "SignedDuration" | "Duration", "time_fn") => hit("Clock"),
4938 ("time", "UtcOffset", "current_local_offset") => hit("Clock"),
4939 ("portable_pty", "CommandBuilder", "new" | "from_argv" | "new_default_prog") => hit("Env"),
4940 ("notify", "PollWatcher", "with_initial_scan") => hit("Clock"),
4941 ("native_tls_crate" | "native_tls", "TlsConnectorBuilder", "build") => hit("Fs"),
4942 ("duct", "Handle", "wait_timeout" | "wait_deadline") => hit("Clock"),
4943 ("dotenvy", "Iter", "load_override") => hit("Env"),
4944 _ => None,
4945 }
4946}
4947
4948/// SOUNDNESS R1528 (b10) — crossterm 0.29.0, ABOVE its 0.28.1 ceiling (`CALIBRATED_CEILINGS` is NOT raised): a `Cargo.lock`
4949/// consumer got `invisible:[crossterm]` on every unmatched call and a lockless one nothing; a rule replaces that disclosure
4950/// with the concrete effect. EXECUTED in a consumer fixture under a DYLD interposer (`getenv`, `open`, `isatty`, `ioctl`,
4951/// `tcgetattr`, `tcsetattr`) and, for the terminal calls, under a pseudo-terminal whose master answers the cursor query
4952/// (handoff `rust-r1528/b10/`).
4953///
4954/// CHARGED `Env`: `Colored::{ansi_color_disabled, ansi_color_disabled_memoized, set_ansi_color_disabled}`,
4955/// `style::force_color_output` (`NO_COLOR`; EXECUTED 1 `getenv NO_COLOR` each; the memoized and the setter read it ONCE per
4956/// process — EXECUTED 0 reads on a second call, an accepted over-charge: the first call is the one every program makes) and
4957/// `style::available_color_count` (`COLORTERM`, then `TERM`; EXECUTED, on every call).
4958/// CHARGED `Ipc`: `cursor::position` (EXECUTED under a pty: `ESC [ 6 n` written to the terminal, raw mode toggled with
4959/// 2 `tcsetattr`, the reply read; with no controlling terminal it opens `/dev/tty` and returns an error), and
4960/// `event::EventStream::{new, next, poll_next}`: `next` is the read (EXECUTED: `block_on(stream.next())` returned the key typed
4961/// on the pty); `new` is charged by this file's own constructor convention (the `reqwest` `bytes_stream` rule) because the
4962/// terminal read hides behind a `Stream::next` a consumer often holds untyped — EXECUTED: `new` itself writes and reads
4963/// nothing, it probes the tty (`isatty`/`ioctl`, or opens `/dev/tty` when stdin is not one, panicking when there is none)
4964/// and spawns the poller thread. An accepted over-charge when the stream is never polled.
4965///
4966/// NOT charged, and filed (R1595): `Print::write_ansi` and `SetTitle::write_ansi` write the caller's `T: Display` into the
4967/// caller's writer (EXECUTED: 0 events for `&str`, and 1 `NO_COLOR` read for `Print(Colored::..)`), so the effect is the type
4968/// argument's; `SetTitle::execute_winapi` and `ansi_support::supports_ansi` are `cfg(windows)` and cannot be executed on
4969/// this host (read only).
4970fn crossterm_r1528(path: &str) -> Option<&'static str> {
4971 let rest = path.strip_prefix("crossterm::")?;
4972 match rest {
4973 "style::available_color_count" | "available_color_count" | "style::force_color_output" | "force_color_output" => {
4974 return Some("Env")
4975 }
4976 "cursor::position" | "position" | "cursor::sys::unix::position" | "cursor::sys::position" => return Some("Ipc"),
4977 _ => {}
4978 }
4979 let (head, method) = rest.rsplit_once("::")?;
4980 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
4981 match (module, ty, method) {
4982 (
4983 "" | "style" | "style::types" | "style::types::colored",
4984 "Colored",
4985 "ansi_color_disabled" | "ansi_color_disabled_memoized" | "set_ansi_color_disabled",
4986 ) => Some("Env"),
4987 ("" | "event" | "event::stream", "EventStream", "new" | "next" | "poll_next") => Some("Ipc"),
4988 _ => None,
4989 }
4990}
4991
4992/// SOUNDNESS R1528 (b10) — reqwest 0.13.5, ABOVE its 0.13.4 ceiling (`CALIBRATED_CEILINGS` is NOT raised). EXECUTED in a
4993/// consumer fixture under a DYLD interposer (`getenv`, `open`) and a local HTTP server (handoff `rust-r1528/b10/`).
4994///
4995/// CHARGED `Env`: `ClientBuilder::build` (async and blocking) and `Client::new`/`Client::build` (the scan attributes a chain
4996/// hop off `Client::builder()` to the root type, so `Client::builder().timeout(..).build()` reaches classify as
4997/// `reqwest::Client::build`): EXECUTED `ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and lowercase) read on EVERY call
4998/// (a second build in the same process reads them again), 0 for `ClientBuilder::new()`; with `HTTP_PROXY` set the call still
4999/// opens nothing but the platform's own configuration plumbing. The `Fs` the self-scan also saw depends on the TLS feature set
5000/// (native roots): no file was opened by `build` on this macOS host with the default features, and Linux was not executed, so
5001/// it is NOT charged. `NoProxy::from_env` reads `NO_PROXY` / `no_proxy` (EXECUTED).
5002/// CHARGED `Fs`: `multipart::Form::file`, async and blocking (EXECUTED: `open(<path>)` is attempted, including for a missing path).
5003fn reqwest_r1528(path: &str) -> Option<&'static str> {
5004 let rest = path.strip_prefix("reqwest::")?;
5005 let (head, method) = rest.rsplit_once("::")?;
5006 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5007 match (module, ty, method) {
5008 ("" | "async_impl::client" | "blocking" | "blocking::client", "ClientBuilder" | "Client", "build") => Some("Env"),
5009 ("" | "blocking" | "blocking::client", "Client", "new") => Some("Env"),
5010 ("" | "multipart" | "async_impl::multipart" | "blocking::multipart", "Form", "file") => Some("Fs"),
5011 ("" | "proxy", "NoProxy", "from_env") => Some("Env"),
5012 _ => None,
5013 }
5014}
5015
5016/// SOUNDNESS R1528 (b10) — mysql 28.0.3, ABOVE its 28.0.0 ceiling (`CALIBRATED_CEILINGS` is NOT raised). EXECUTED against
5017/// MySQL 8 (docker) behind a counting TCP proxy that the fixture resets and reads at its own start and end markers, beside
5018/// an idle control (0 conns, 0 bytes) (handoff `rust-r1528/b10/`).
5019///
5020/// CHARGED `Db`: `Conn::reset` (5 bytes up / 11 down), `Conn::change_user` and `PooledConn::change_user` (110 / 65),
5021/// `Pool::try_get_conn` (10 / 22 on an idle pool), `Pool::start_transaction` (40 / 33), `Conn::start_transaction` and
5022/// `PooledConn::start_transaction` (35 / 22; never rows), `Conn::get_binlog_stream` and `PooledConn::get_binlog_stream`
5023/// (feature `binlog`; COM_BINLOG_DUMP, 80 / 22), and `Pool::new`: EXECUTED 10 connections and 4,040 bytes with the default
5024/// pool constraints (min 10). The accepted over-charge is `PoolConstraints::new(0, ..)`, with which `Pool::new` connects
5025/// nothing (EXECUTED 0 / 0): the default is the idiom that connects.
5026///
5027/// READ PURE, `REVIEWED_PURE_ENTRIES`: `PooledConn::unwrap` (EXECUTED 0 conns, 0 bytes: it moves the `Conn` out).
5028fn mysql_r1528(path: &str) -> Option<&'static str> {
5029 let rest = path.strip_prefix("mysql::")?;
5030 let (head, method) = rest.rsplit_once("::")?;
5031 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5032 match (module, ty, method) {
5033 ("" | "conn", "Conn", "reset" | "change_user" | "start_transaction" | "get_binlog_stream") => Some("Db"),
5034 ("" | "conn::pool", "PooledConn", "change_user" | "start_transaction" | "get_binlog_stream") => Some("Db"),
5035 ("" | "conn::pool", "Pool", "new" | "try_get_conn" | "start_transaction") => Some("Db"),
5036 _ => None,
5037 }
5038}
5039
5040/// SOUNDNESS R1528 (b10) — diesel 2.3.14, ABOVE its 2.3.13 ceiling (`CALIBRATED_CEILINGS` is NOT raised). EXECUTED against a
5041/// file-backed SQLite (bundled) and PostgreSQL 16 (docker), the ground truth read from the database itself (handoff
5042/// `rust-r1528/b10/`).
5043///
5044/// CHARGED `Db`: `batch_execute` (EXECUTED: the table it creates exists afterwards, on SQLite), `SqliteConnection::
5045/// {immediate_transaction, exclusive_transaction}` and `AnsiTransactionManager::begin_transaction_sql` (EXECUTED: a second
5046/// connection's write fails with `database is locked` inside them and succeeds inside a deferred `transaction`),
5047/// `SqliteConnection::serialize_database_to_buffer` (8,192 bytes of the database image) and `deserialize_readonly_database_
5048/// from_buffer` (EXECUTED: the table of the buffer's database exists afterwards, the connection's own table does not), and
5049/// `TransactionBuilder::run` (EXECUTED on PostgreSQL: `current_setting('transaction_read_only')` is `on` inside it), and the
5050/// sibling `Connection::transaction` (below). The
5051/// scan attributes `conn.build_transaction().read_only().run(..)` to the ROOT type, so the same rule keys on
5052/// `PgConnection::run`, which has no such method of its own.
5053fn diesel_r1528(path: &str) -> Option<&'static str> {
5054 let rest = path.strip_prefix("diesel::")?;
5055 let (head, method) = rest.rsplit_once("::")?;
5056 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5057 match (module, ty, method) {
5058 ("" | "sqlite" | "sqlite::connection" | "pg" | "pg::connection" | "connection", "SqliteConnection" | "PgConnection" | "SimpleConnection", "batch_execute") => Some("Db"),
5059 (
5060 "" | "sqlite" | "sqlite::connection",
5061 "SqliteConnection",
5062 "immediate_transaction" | "exclusive_transaction" | "serialize_database_to_buffer" | "deserialize_readonly_database_from_buffer",
5063 ) => Some("Db"),
5064 ("" | "connection" | "connection::transaction_manager", "AnsiTransactionManager", "begin_transaction_sql") => Some("Db"),
5065 // `Connection::transaction`, a sibling the generator never listed (a provided trait method): EXECUTED on PostgreSQL, the
5066 // closure ran inside an explicit transaction (`clock_timestamp() - now()` > 80 ms after a 100 ms sleep) and in the
5067 // autocommit control it did not. SQLite takes the same `AnsiTransactionManager` path (READ).
5068 ("" | "sqlite" | "sqlite::connection" | "pg" | "pg::connection" | "connection", "SqliteConnection" | "PgConnection" | "Connection", "transaction") => {
5069 Some("Db")
5070 }
5071 ("" | "pg" | "pg::transaction", "TransactionBuilder", "run") => Some("Db"),
5072 ("" | "pg" | "pg::connection", "PgConnection", "run") => Some("Db"),
5073 _ => None,
5074 }
5075}
5076
5077/// SOUNDNESS R1528 — lapin 4.12.0, ABOVE its 4.10.0 ceiling (`CALIBRATED_CEILINGS` is NOT raised: that needs the whole
5078/// crate table re-verified at 4.12). A `Cargo.lock` consumer therefore already got `invisible:[lapin]` on every unmatched
5079/// lapin call and a lockless one nothing; a rule here replaces that disclosure with the concrete effect. EXECUTED against a
5080/// real RabbitMQ 3 (docker, behind a counting TCP proxy that the fixture reads at its own start and end markers, so the
5081/// delta is exact) beside an idle control (handoff `rust-r1528/mq/`).
5082///
5083/// CHARGED `Net`: every connection opener the existing `::connect` rule did not reach, `Connection::{connect_uri,
5084/// connect_uri_with_runtime, connect_uri_with_config}` and the `AMQPUri` / `Connect` `connect_with_config`, plus the two
5085/// siblings that were never rows, `Connection::{connect_with_config, connect_with_runtime}` (EXECUTED: each opened exactly
5086/// 1 connection and moved 373 bytes up / 567 down); `Connection::update_secret` (19 up / 12 down); the generated
5087/// `Channel::{access_request, channel_flow}` that the `::basic_` / `::queue_` / `::exchange_` family never covered (19 / 13
5088/// bytes up); and `Acker::{ack, nack, reject}` (21 bytes up: the `basic.ack` / `basic.nack` / `basic.reject` frame).
5089///
5090/// READ PURE, `REVIEWED_PURE_ENTRIES`: `Channel::wait_for_confirms` sends nothing (EXECUTED: 0 bytes up with nothing pending
5091/// and with a publish pending; the 21 bytes DOWN in the second case are the confirmation, read by the IO loop thread that
5092/// `connect` started, and `basic_publish` carries the `Net`).
5093///
5094/// NOT charged, and filed (R1593): `Connection::connector` runs a CALLER-SUPPLIED connect closure (EXECUTED: 1
5095/// connection with a closure that dials, 0 bytes with one that returns an error), `Connection::run` (EXECUTED: 0 bytes up
5096/// while another handle is alive, but 21 up / 12 down when it holds the LAST handle: `drop(self)` closes the connection)
5097/// and `Consumer::set_delegate` (EXECUTED: it runs the caller's delegate on every queued delivery, 0 bytes of its own).
5098fn lapin_r1528(path: &str) -> Option<&'static str> {
5099 let rest = path.strip_prefix("lapin::")?;
5100 let (head, method) = rest.rsplit_once("::")?;
5101 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5102 match (module, ty, method) {
5103 (
5104 "" | "connection",
5105 "Connection",
5106 "connect_uri" | "connect_uri_with_runtime" | "connect_uri_with_config" | "connect_with_config"
5107 | "connect_with_runtime" | "update_secret",
5108 ) => Some("Net"),
5109 ("" | "connection" | "uri", "AMQPUri" | "Connect", "connect_with_config") => Some("Net"),
5110 ("" | "channel" | "generated::channel", "Channel", "access_request" | "channel_flow") => Some("Net"),
5111 ("" | "acker", "Acker", "ack" | "nack" | "reject") => Some("Net"),
5112 _ => None,
5113 }
5114}
5115
5116/// SOUNDNESS R1528 — tonic 0.14.6, ABOVE its 0.12.3 ceiling (`CALIBRATED_CEILINGS` is NOT raised: that needs the whole
5117/// crate table re-verified at 0.14). A `Cargo.lock` consumer already got `invisible:[tonic]` on every unmatched tonic
5118/// call and a lockless one nothing. EXECUTED in a consumer fixture under a `bind` / `listen` / `connect` / `getenv` / `open`
5119/// interposer, with a probe that dials the port before and after the call (handoff `rust-r1528/mq/`).
5120///
5121/// THE LOAD-BEARING FINDING: the quickstart `Server::builder().add_service(svc).serve(addr).await` reaches classify as
5122/// `tonic::transport::Server::serve` — the scan attributes `.add_service(..)` and the chain after it to the ROOT type
5123/// `Server`, not to `Router` — so the existing `Router::serve` rule never fired for it, and the rows that name `Server::
5124/// serve` are the common idiom, not an obscure one. EXECUTED: `serve` and `serve_with_shutdown` bound and listened on
5125/// the port (the probe could not connect before and could after), in the builder spelling and the quickstart chain.
5126///
5127/// CHARGED `Net`: `Server::{serve, serve_with_shutdown}` (and so the quickstart chain) and `TcpIncoming::bind` (EXECUTED:
5128/// `bind` + `listen` logged; 0.14.6 has no `TcpIncoming::new`, which is what the older rule named).
5129///
5130/// NOT charged, and filed (R1594): `serve_with_incoming{,_shutdown}` on `Server` and `Router` serve whatever stream they are
5131/// handed (EXECUTED: over a `TcpIncoming` a client connected and got a reply; over an in-memory `duplex` stream the request was
5132/// served with no `bind`, `listen` or `connect` at all); `Endpoint::tls_config{,_with_verifier}` read `SSL_CERT_FILE` /
5133/// `SSL_CERT_DIR` and the root store only for `ClientTlsConfig::with_native_roots()` (EXECUTED: opens of the bundle with
5134/// it, none with `with_webpki_roots()` or a bare config), and for `use_key_log()` read `SSLKEYLOGFILE` and open the file
5135/// (EXECUTED, `tls_config_with_verifier`; native roots plus a verifier is an error before any read); `Code::from_bytes`
5136/// and `Status::from_header_map` log (`trace!` / `warn!`) only on malformed input (EXECUTED: 0 records valid, 1 invalid);
5137/// `Streaming::{message, trailers}` read the body they were given (EXECUTED: `message` returned a status the server pushed
5138/// 1.75 s later; `trailers` did not return within 1.2 s on a live stream).
5139fn tonic_r1528(path: &str) -> Option<&'static str> {
5140 let rest = path.strip_prefix("tonic::")?;
5141 let (head, method) = rest.rsplit_once("::")?;
5142 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5143 match (module, ty, method) {
5144 ("" | "transport" | "transport::server" | "server", "Server", "serve" | "serve_with_shutdown") => Some("Net"),
5145 ("" | "transport" | "transport::server" | "transport::server::incoming" | "server::incoming", "TcpIncoming", "bind") => {
5146 Some("Net")
5147 }
5148 _ => None,
5149 }
5150}
5151
5152/// SOUNDNESS R1528 — aws-config 1.12.0 (the version the rules were written against AND the resolved one: ceiling
5153/// 1.12.0, so a `Cargo.lock` consumer and a lockless one both got NOTHING for these rows — in-ceiling exemption).
5154/// Every body READ, and each EXECUTED in a consumer fixture under a DYLD interposer that logs every `getenv`, `open`
5155/// and `connect` the process makes, beside a local IMDS / ECS mock that counts requests (handoff `rust-r1528/awsq/`).
5156///
5157/// CHARGED `Fs` — the profile-file readers. `ProviderConfig::profile()` loads `~/.aws/config` and `~/.aws/credentials`
5158/// (EXECUTED: both opened, with `AWS_CONFIG_FILE` / `AWS_SHARED_CREDENTIALS_FILE` / `AWS_PROFILE` read first) and the
5159/// setting's own `AWS_*` variable is read beside it: `endpoint_url_provider{,_with_origin}`,
5160/// `ignore_configured_endpoint_urls_provider`, `{request_checksum_calculation,response_checksum_validation}_provider`,
5161/// `use_dual_stack_provider`, `use_fips_provider`, `app_name::Builder::app_name`, `ProfileFileRegionProvider::region`,
5162/// and a sibling the generator never listed because its body has no `tracing` call: `retry_config::Builder::retry_config`
5163/// (EXECUTED the same). `ProviderConfig`'s `env` / `fs` setters are `pub(crate)`, so no consumer can hand one a fake
5164/// environment: the reads are always real.
5165///
5166/// CHARGED `Net` — the verbs that can reach IMDS or STS, the effect `.load()` already carries: the default region chain
5167/// (`DefaultRegionChain::region`, `ProviderConfig::{load_default_region, with_default_region}`) asks IMDS when the
5168/// environment and the profile give no region; `ImdsRegionProvider::region` always does; `default_provider::{credentials,
5169/// token}::Builder::build` resolve that region chain first; `AssumeRoleProvider::{build, build_from_provider}` and
5170/// `SsoTokenProvider::build` call `load_defaults(..)` unless an `SdkConfig` was supplied; `EcsCredentialsProvider::
5171/// credentials` GETs the container-credentials URL. EXECUTED: with no region anywhere the mock IMDS counted a token PUT
5172/// and a region GET for each of them (`assume build` 3 hits), and with a profile region none — so the `Net` is
5173/// STATE-CONDITIONAL, kept for the reason `load` is (the default idiom with no configured region does reach it).
5174/// Two over-charges follow and are accepted: a `.region(Region::new(..))` override (EXECUTED: 3 `AWS_*` reads, no file, no
5175/// socket) and `AssumeRoleProvider::builder(..).configure(&sdk).build()` (EXECUTED: nothing) read pure.
5176/// `credentials::default_provider` and `token::default_provider` (not rows; the same `Builder::build` behind a free fn)
5177/// are charged with them.
5178/// 2026-11-13 refresh: `CredentialsProviderChain::{default_provider, or_default_provider}` and `TokenProviderChain::{..}` are
5179/// `first_try(.., default_provider().await)` / `or_else(.., default_provider().await)` (meta/credentials/chain.rs:79-93,
5180/// meta/token.rs:70-84), so they are those free fns' effect and charged with them. The scan used to DROP the
5181/// `DefaultCredentialsChain::builder().build()` edge inside the free fn (R1717's builder typing restored it, which is what grew
5182/// the four rows as `Log,Unknown`), so the reading above carries over unchanged: `Net`, state-conditional.
5183///
5184/// ONE EFFECT IS ALL `classify` CAN CARRY (R1575 class): every `Fs` row also reads `AWS_*` (`Env`) and every `Net` row also
5185/// reads the environment and the profile files (`Env`, `Fs`); only the one named is charged.
5186///
5187/// NOT charged, and filed (R1588): `RegionProviderChain::region` and `Box<dyn ProvideRegion>::region` forward to whichever
5188/// providers the chain holds (`conditional`: EXECUTED `RegionProviderChain::default_provider()` reaches the env, the profile
5189/// and IMDS and `first_try(Region::new(..))` reaches nothing), and `profile::{credentials, token}::Builder::build` only
5190/// construct a provider whose I/O is its later `provide_credentials` / `provide_token` (`silent`: a rule there would charge
5191/// the span `tracing::debug_span!` and one `AWS_EXECUTION_ENV` read and, above the ceiling, delete the `invisible` that is
5192/// the only disclosure of that later I/O). The scan types `.build()` after `builder()` as the root type, so the paths
5193/// below are the root-typed spellings as well as the declaring `Builder` ones.
5194fn aws_config_r1528(path: &str) -> Option<&'static str> {
5195 let rest = path.strip_prefix("aws_config::")?;
5196 match rest {
5197 "default_provider::endpoint_url::endpoint_url_provider"
5198 | "default_provider::endpoint_url::endpoint_url_provider_with_origin"
5199 | "default_provider::ignore_configured_endpoint_urls::ignore_configured_endpoint_urls_provider"
5200 | "default_provider::checksums::request_checksum_calculation_provider"
5201 | "default_provider::checksums::response_checksum_validation_provider"
5202 | "default_provider::use_dual_stack::use_dual_stack_provider"
5203 | "default_provider::use_fips::use_fips_provider" => return Some("Fs"),
5204 "default_provider::credentials::default_provider" | "default_provider::token::default_provider" => {
5205 return Some("Net")
5206 }
5207 _ => {}
5208 }
5209 let (head, method) = rest.rsplit_once("::")?;
5210 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5211 match (module, ty, method) {
5212 ("default_provider::app_name", "Builder", "app_name")
5213 | ("default_provider::retry_config", "Builder", "retry_config")
5214 | ("profile::region", "ProfileFileRegionProvider", "region") => Some("Fs"),
5215 ("default_provider::region", "DefaultRegionChain", "region")
5216 | ("imds::region", "ImdsRegionProvider", "region")
5217 | ("provider_config", "ProviderConfig", "load_default_region" | "with_default_region")
5218 | ("default_provider::credentials", "DefaultCredentialsChain" | "Builder", "build")
5219 | ("default_provider::token", "DefaultTokenChain" | "Builder", "build")
5220 | ("sts" | "sts::assume_role", "AssumeRoleProvider" | "AssumeRoleProviderBuilder", "build" | "build_from_provider")
5221 | ("sso::token", "SsoTokenProvider" | "Builder", "build")
5222 | ("ecs", "EcsCredentialsProvider", "credentials")
5223 // the chain constructors that `.await` the free `default_provider` above (aws-config 1.13.2; the 2026-11-13 refresh)
5224 // (`""` is the crate-root spelling the coverage-gate's consumer-path guess records, `aws_config::TokenProviderChain::..`)
5225 | ("meta::credentials::chain" | "meta::credentials" | "", "CredentialsProviderChain", "default_provider" | "or_default_provider")
5226 | ("meta::token" | "", "TokenProviderChain", "default_provider" | "or_default_provider") => Some("Net"),
5227 _ => None,
5228 }
5229}
5230
5231/// SOUNDNESS R1528 — sqlx-core 0.9.0 (ceiling 0.9.0, resolved 0.9.0: in-ceiling, so both arms got nothing). EXECUTED
5232/// against a real postgres 16 (docker) reached through a counting TCP proxy with idle windows beside it, plus a `log`
5233/// logger and a `clock_gettime` interposer for the two logger rows (handoff `rust-r1528/awsq/`). A `sqlx` FACADE
5234/// consumer spells these `sqlx::PgPool::begin_with` / `sqlx::pool::PoolConnection::close` — crate `sqlx`, not
5235/// `sqlx_core`, so the `sqlx_core` rows are reached by a facade user only through this function's `sqlx` arm.
5236///
5237/// * `Db`: `Pool::begin_with` (35 bytes up, 49 down: acquire + `BEGIN`), `PoolConnection::return_to_pool` (5 up, 6
5238/// down: the unconditional `ping` in `Floating::return_to_pool`, pool/connection.rs:275-309).
5239/// * `Net`: `PoolConnection::close` (5 bytes up: the protocol `Terminate`, then the socket closes) and `net::connect_tcp`
5240/// (1 connection: the consumer spelling is the `net::` re-export, which no rule matched; the file-path spelling
5241/// `net::socket::connect_tcp` is recognised too).
5242/// * `Fs`: `Migrator::new` over a path, `fs::ReadDir::next`, and `migrate::source::resolve_blocking{,_with_config}`
5243/// (the file-path spelling of the two the `::migrate::resolve_blocking` rule already charges). EXECUTED: the result
5244/// follows the directory (1 migration, then 2 after a file is added; an error on a missing path).
5245/// * `Clock`: `logger::QueryLogger::new` (`Instant::now()`, EXECUTED 1 `clock_gettime` against 0 for an empty bracket).
5246/// * `Log`: `QueryLogger::finish` (EXECUTED: 1 record on target `sqlx::query` at the default statement level; the
5247/// record is gated on the active level, charged like `tracing::info!`).
5248///
5249/// READ PURE, `REVIEWED_PURE_ENTRIES`: `PoolConnection::{detach, leak}` return the inner connection and drop a size guard;
5250/// the doc comment's "a task will be spawned to replace this connection" is not in the body (EXECUTED: 0 bytes, 0
5251/// connections).
5252///
5253/// NOT charged, and filed (R1589): `Pool::connect_lazy{,_with}` / `PoolOptions::connect_lazy{,_with}` (`conditional`:
5254/// EXECUTED 0 connections in 5 spellings, but 1 connection and 411 bytes up when `min_connections(1)` is set with both
5255/// timeouts disabled and the pool is HELD, because the maintenance task is spawned), the four `Box<S: Socket>` `poll_*`
5256/// forwarders (`conditional`: the socket type's own effect; the consumer spelling is the trait path `Socket::poll_*`),
5257/// and `MigrationSource::resolve` for `PathBuf` / `ResolveWith` (`silent`: no spelling reaches them — `p.resolve()` on a
5258/// `std::path::PathBuf` or on a `ResolveWith` tuple struct is untyped to the scan, EXECUTED to read the directory).
5259fn sqlx_r1528(krate: &str, path: &str) -> Option<&'static str> {
5260 let rest = path.strip_prefix(krate)?.strip_prefix("::")?;
5261 if krate == "sqlx_core" {
5262 match rest {
5263 "net::connect_tcp" | "net::socket::connect_tcp" => return Some("Net"),
5264 "migrate::source::resolve_blocking" | "migrate::source::resolve_blocking_with_config" => {
5265 return Some("Fs")
5266 }
5267 _ => {}
5268 }
5269 }
5270 let (head, method) = rest.rsplit_once("::")?;
5271 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5272 let pool = ty == "Pool" || (krate == "sqlx" && ty.ends_with("Pool"));
5273 if pool && matches!(module, "" | "pool" | "postgres" | "mysql" | "sqlite" | "any") && method == "begin_with" {
5274 return Some("Db");
5275 }
5276 // SOUNDNESS R1618 — `connect_with` is the sqlx facade's OTHER connection-opening verb. The facade's `VERBS` list names
5277 // `::connect` only, so `MySqlConnection::connect_with(&opts)`, `MySqlPool::connect_with(opts)` and
5278 // `MySqlPoolOptions::connect_with(opts)` read PURE (absent from `functions[]`, no `invisible`; the lane that filed
5279 // R1618 found them while executing the `::connect` rows). EXECUTED against mysql:8 (macOS, DYLD interposer): each opens a
5280 // TCP connection and draws TLS entropy, exactly as the `::connect` spelling. Keyed on module + type + method: the
5281 // facade's root re-export or a driver module, a `*Connection` / `*Pool` / `*PoolOptions` type.
5282 if krate == "sqlx"
5283 && method == "connect_with"
5284 && matches!(module, "" | "pool" | "mysql" | "postgres" | "sqlite" | "any")
5285 && (ty.ends_with("Connection") || ty.ends_with("Pool") || ty.ends_with("PoolOptions"))
5286 {
5287 return Some("Db");
5288 }
5289 // SOUNDNESS R1623 — sqlx 0.9.0 Postgres/SQLite/pool entries that read pure or silent. See `sqlx_r1623`.
5290 if krate == "sqlx" {
5291 if let Some(effect) = sqlx_r1623(module, ty, method) {
5292 return Some(effect);
5293 }
5294 }
5295 match (module, ty, method) {
5296 ("" | "pool" | "pool::connection", "PoolConnection", "close") => Some("Net"),
5297 ("" | "pool" | "pool::connection", "PoolConnection", "return_to_pool") => Some("Db"),
5298 ("" | "migrate" | "migrate::migrator", "Migrator", "new") => Some("Fs"),
5299 // SOUNDNESS R1528 (sqlx-core 0.9.0 refresh, covgate lane) — the four migration RUNNERS. EXECUTED (sqlite file db behind
5300 // `connect_lazy`, so the file exists only if the verb touched it): `run`, `run_to`, `skip`, `undo` and the doc-hidden
5301 // `run_direct` each created the db file and `_sqlx_migrations` (`run`/`run_to`/`run_direct` also applied the migration:
5302 // table `t1`; `skip` marked it applied: 1 row; `undo` ensured the table: 0 rows); the pure control `Migrator::iter` and a
5303 // constructed-but-not-run `Migrator`: no file. The scan could not reach this itself: the connection is the caller's
5304 // `A: Acquire` and the `Migrate` impl is dispatched, so the oracle nominated `Clock,Log,Unknown` and no rule named `Db`.
5305 ("" | "migrate" | "migrate::migrator", "Migrator", "run" | "run_to" | "skip" | "undo" | "run_direct") => Some("Db"),
5306 ("fs", "ReadDir", "next") if krate == "sqlx_core" => Some("Fs"),
5307 ("logger", "QueryLogger", "new") if krate == "sqlx_core" => Some("Clock"),
5308 ("logger", "QueryLogger", "finish") if krate == "sqlx_core" => Some("Log"),
5309 _ => None,
5310 }
5311}
5312
5313/// SOUNDNESS R1623 — the sqlx 0.9.0 entries R1618 left (its residual (c)), EXECUTED on macOS (DYLD interposer) against
5314/// postgres:16 (TLS on, SCRAM), mysql:8 and real sqlite files, plus the postgres server's `log_statement=all` for the one
5315/// round-trip the interposer cannot see (handoff `rust-r1623/`). Primary effects only; the second effects are `ALSO_EFFECTS`.
5316///
5317/// * `Env` (PRIMARY): `PgConnectOptions::{new, new_without_pgpass, default, from_str, from_url}` read the twelve `PG*`
5318/// variables (14 with `.pgpass`: `HOME`/`PGPASSFILE`), and so does `PgPool::connect_lazy(url)` /
5319/// `PgPoolOptions::connect_lazy(url)`, whose URL is parsed with no connect. EXECUTED: 12 `getenv` + 3 `stat` on every one,
5320/// 0 connections for the lazy pair. All were ABSENT, so a `deny Env` over code that builds Postgres options passed.
5321/// `connect_lazy_with(opts)` parses nothing and stays pure, as do the option setters (`host`, `port`, `ssl_mode`, ...).
5322/// * `Db`: `Pool::try_begin` (EXECUTED against `log_statement=all`: a warm pool's `try_begin` sent `BEGIN`; the cold pool's
5323/// returns `None` with no I/O, which is the stated over-charge) and `PgListener::connect_with(&pool)` (a COLD pool connects:
5324/// 1 connect, 6 `getentropy`; a warm pool does not). `try_acquire` stays pure (EXECUTED: 0 events cold and warm).
5325///
5326/// The facade spells a driver type under the root (`sqlx::PgPool`) or its driver module (`sqlx::postgres::PgPool`); both are
5327/// accepted. A generic `sqlx::Pool<DB>` is not claimed: the driver, and so the effect, is not in the path.
5328fn sqlx_r1623(module: &str, ty: &str, method: &str) -> Option<&'static str> {
5329 if !matches!(module, "" | "postgres" | "mysql" | "sqlite" | "any" | "pool") {
5330 return None;
5331 }
5332 let pg_mod = matches!(module, "" | "postgres");
5333 match (ty, method) {
5334 ("PgConnectOptions", "new" | "new_without_pgpass" | "default" | "from_str" | "from_url") if pg_mod => Some("Env"),
5335 ("PgPool" | "PgPoolOptions", "connect_lazy") if pg_mod => Some("Env"),
5336 ("PgListener", "connect_with") if pg_mod => Some("Db"),
5337 (t, "try_begin") if t == "Pool" || t.ends_with("Pool") => Some("Db"),
5338 _ => None,
5339 }
5340}
5341
5342/// SOUNDNESS R1528 — redis 1.7.1, ABOVE the 1.6.0 ceiling (`CALIBRATED_CEILINGS` is NOT raised: that needs the whole crate
5343/// table re-verified at 1.7.1, which this lane did not do). A `Cargo.lock` consumer therefore already got
5344/// `invisible:[redis]` on every unmatched redis call and a lockless one nothing; a rule here replaces that disclosure
5345/// with the concrete effect. EXECUTED against a real redis 7 (docker, standalone and a 3-master cluster), the server's
5346/// `total_commands_processed` / `total_connections_received` read around each call beside an idle control, a counting TCP
5347/// proxy where the stream is caller-supplied, and an entropy interposer for the `Rand` rows (handoff `rust-r1528/awsq/`).
5348///
5349/// CHARGED `Db` (the redis rules' own class for a connection or a command): `Client::{is_open, check_connection}` (both
5350/// OPEN A NEW CONNECTION: +1 connection, +2 and +3 commands — `is_open` is not a flag read on a `Client`),
5351/// `Client::{get_async_pubsub, get_async_monitor}`, `Cmd::iter_async` (+1 `SCAN`), `aio::MultiplexedConnection::
5352/// {send_packed_command, send_packed_commands}` (+1 command, 0 connections), the sync and async `ClusterConnection::
5353/// route_command` and `ClusterPipeline::exec` (+1 command each), `MultiplexedConnection::connect_with_config` (+1
5354/// connection, +2 commands) and `ClusterClient::get_pending_async_connection_with_config` (+13 commands and +3
5355/// connections within two seconds of being HELD, never used: the pending connection is a background task).
5356/// CHARGED `Rand`: `Replicas::choose_random` and `RandomReplicaStrategy::route_read` (`rand::rng()`; EXECUTED 1 OS
5357/// entropy read on a fresh thread against 0 for an idle one; `route_read` is READ only — `ReadCandidates` has no public
5358/// constructor, so a consumer reaches it only by delegating from its own `ReadRoutingStrategy`).
5359///
5360/// READ PURE, `REVIEWED_PURE_ENTRIES`: `Client::build_with_tls` and `ClusterClientBuilder::build` parse PEM bytes the caller
5361/// SUPPLIED (`retrieve_tls_certificates`) and assemble a struct; EXECUTED `build_with_tls` made no server contact.
5362///
5363/// NOT charged, and filed (R1590): `Sentinel::build`, `SentinelClient::build` and `SentinelClientBuilder::build` (`silent`:
5364/// they draw a random replica index, EXECUTED, but a `Rand` rule on the constructor deleted the lock arm's `invisible:[redis]`
5365/// and left the `Db` of the `master_for` / `get_connection` that follows unreported: the receiver is untyped after the `?`),
5366/// `MultiplexedConnection::{new, new_with_config}` (`conditional`: the handshake is
5367/// written to the caller's stream — EXECUTED 110 bytes through a TCP proxy and the same 110 bytes into an in-memory
5368/// `duplex` peer with no server), `EntraIdCredentialsProvider::start` (`conditional`: it spawns a loop over the
5369/// caller-supplied `TokenCredential`; READ only, no Azure to run it against).
5370///
5371/// One effect is all `classify` can carry: connection openers also move bytes (`Net`) and the cluster verbs draw `Rand`.
5372fn redis_r1528(path: &str) -> Option<&'static str> {
5373 let rest = path.strip_prefix("redis::")?;
5374 let (head, method) = rest.rsplit_once("::")?;
5375 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5376 let cluster_mod = matches!(
5377 module,
5378 "cluster" | "cluster_async" | "cluster_handling::sync_connection" | "cluster_handling::async_connection"
5379 );
5380 let hit = match ty {
5381 "Client" => {
5382 matches!(module, "" | "client")
5383 && matches!(method, "is_open" | "check_connection" | "get_async_pubsub" | "get_async_monitor")
5384 }
5385 "Cmd" => matches!(module, "" | "cmd") && method == "iter_async",
5386 // SOUNDNESS R1528 (redis 1.8.1 refresh, covgate lane): the SYNC `PubSub`'s command verbs — each writes a command and reads
5387 // its reply through `cache_messages_until_received_response` (connection.rs:2232-2266). EXECUTED against redis:7 behind a
5388 // counting proxy, idle window beside each: subscribe 27 up / 31 down, psubscribe 29/33, unsubscribe 30/34, punsubscribe 31/35,
5389 // ping 14/7, ping_message 21/7; the pure control and an idle window 0/0. `Db` like the `Commands` verbs (these ISSUE a
5390 // command), where `PubSub::get_message` above only reads. The sync module guard keeps the async `aio` pub/sub out.
5391 "PubSub" => {
5392 matches!(module, "" | "connection")
5393 && matches!(method, "subscribe" | "psubscribe" | "unsubscribe" | "punsubscribe" | "ping" | "ping_message")
5394 }
5395 "MultiplexedConnection" => {
5396 (matches!(module, "aio" | "aio::multiplexed_connection")
5397 && matches!(method, "send_packed_command" | "send_packed_commands" | "connect_with_config"))
5398 || (cluster_mod && method == "connect_with_config")
5399 }
5400 "ClusterConnection" => cluster_mod && method == "route_command",
5401 "ClusterPipeline" => {
5402 (cluster_mod || module == "cluster_handling::sync_connection::pipeline") && method == "exec"
5403 }
5404 "ClusterClient" => {
5405 matches!(module, "cluster" | "cluster_handling::client")
5406 && method == "get_pending_async_connection_with_config"
5407 }
5408 _ => false,
5409 };
5410 if hit {
5411 return Some("Db");
5412 }
5413 let rand = match ty {
5414 "RandomReplicaStrategy" => {
5415 matches!(module, "cluster_read_routing" | "cluster_handling::read_routing::random_replica")
5416 && method == "route_read"
5417 }
5418 "Replicas" => {
5419 matches!(module, "cluster_read_routing" | "cluster_handling::read_routing::interface")
5420 && method == "choose_random"
5421 }
5422 _ => false,
5423 };
5424 rand.then_some("Rand")
5425}
5426
5427/// SOUNDNESS R1528 — the async-nats 0.50.0 broker surface the verb list in `classify` cannot see by name
5428/// (`get_stream`, `put`, `info`, `keys`, `messages`, `add`, `double_ack`...). The `async_nats` arm already charges
5429/// every `::publish*`/`::request*`/`::subscribe`/`::flush`/`::connect` suffix; these are the verbs that list misses.
5430///
5431/// Matching is on the RECEIVER TYPE NAME plus the method, because the method names are generic (`get`, `put`,
5432/// `info`, `list`, `delete`) and a bare suffix would fabricate. The module is taken from the consumer's own spelling
5433/// (`async_nats::jetstream::Context`, `...::kv::Store`, `...::consumer::PullConsumer`; TRACED: the scan hands
5434/// `classify` exactly the path the consumer wrote, including the `PullConsumer` alias and the receiver type a
5435/// builder CHAIN inherits, so `stream.direct_get_builder().sequence(1).send()` arrives as `Stream::send` and
5436/// `consumer.fetch().max_messages(2).messages()` as `PullConsumer::messages`).
5437///
5438/// EXECUTED against nats-server with JetStream, one fixture function per row, the server's received messages /
5439/// bytes / subscription count read before and after (`/varz`): every entry below moved at least one of the three
5440/// (`Net`), and the pure constructors did not move any (`jetstream::{new,with_domain,with_prefix}`,
5441/// `ContextBuilder::build`, `Client::new_inbox`, `datetime::now`). The cases worth stating:
5442/// * `Context::get_stream_no_info` does NOT touch the broker (EXECUTED: zero messages, zero bytes, zero
5443/// subscriptions; it builds an empty `Stream` handle) and is NOT claimed.
5444/// * `Store::status`, `Service::{info,stats,reset}`, `Context::wait_for_acks` and the builder constructors
5445/// (`Consumer::{stream,fetch,batch}`, `Stream::{info_builder,direct_get_builder,raw_message_builder}`,
5446/// `Service::{group,endpoint_builder}`) read cached/local state and are NOT claimed.
5447/// * `Consumer::messages` (pull, push, ordered), `Service::endpoint`, `Group::endpoint`, `EndpointBuilder::add`,
5448/// `ServiceBuilder::start` and `ServiceExt::add_service` are a `SUB` to the broker (EXECUTED: the server's
5449/// subscription count rose by one or more while the handle was held), not a message; `Net` is the existing
5450/// family's answer for `::subscribe` and these are that call one hop down.
5451/// * `jetstream::new`, `with_domain`, `with_prefix` and `ContextBuilder::build` (they share one body) open no
5452/// connection and are DELIBERATELY NOT RULES (R1576). Their own effect is a `Log` when the `Context` is dropped
5453/// (EXECUTED with a tracing layer: `spawn_acker`'s `debug!("Acker task exited")`), but the engine does not type
5454/// `let js = jetstream::new(c)` (a dependency's free fn has no entry in the `ReturnIndex`), so every
5455/// `js.<verb>()` after it reaches no classify path. MEASURED both arms: with a `Log` rule the function
5456/// `let js = jetstream::new(c); js.query_account().await` goes from `invisible:[async_nats]` to `['Log']` alone,
5457/// so the rule would delete the only disclosure of a `Net` it cannot see.
5458/// * `Client::new_inbox` is a `nuid::next()` draw (EXECUTED: two calls never agree) and `datetime::now` is
5459/// `OffsetDateTime::now_utc()` (EXECUTED: it advances).
5460///
5461/// One effect is all `classify` can carry: these calls also draw an inbox nonce (`Rand`) and most emit two to four
5462/// `debug!` events (`Log`, EXECUTED through a span-scoped tracing layer); neither is charged (R1575).
5463fn async_nats_r1528(path: &str) -> Option<&'static str> {
5464 let rest = path.strip_prefix("async_nats::")?;
5465 if rest == "datetime::now" {
5466 return Some("Clock");
5467 }
5468 let (head, method) = rest.rsplit_once("::")?;
5469 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5470 let js = |m: &str| module == "jetstream" || module == m;
5471 let hit = match ty {
5472 "Context" => {
5473 js("jetstream::context")
5474 && matches!(
5475 method,
5476 "query_account" | "create_stream" | "get_stream" | "get_or_create_stream" | "delete_stream"
5477 | "update_stream" | "create_or_update_stream" | "stream_by_subject" | "send_publish"
5478 | "send_request" | "get_key_value" | "create_key_value" | "update_key_value"
5479 | "create_or_update_key_value" | "delete_key_value" | "get_consumer_from_stream"
5480 | "delete_consumer_from_stream" | "create_consumer_on_stream" | "update_consumer_on_stream"
5481 | "create_consumer_strict_on_stream" | "create_object_store" | "get_object_store"
5482 | "delete_object_store"
5483 )
5484 }
5485 // `send` / `fetch` are the DirectGetBuilder / RawMessageBuilder / StreamInfoBuilder terminals, which a
5486 // builder chain (or a bound builder) reports under the `Stream` receiver it started from.
5487 "Stream" | "DirectGetBuilder" | "RawMessageBuilder" | "StreamInfoBuilder" => {
5488 js("jetstream::stream")
5489 && matches!(
5490 method,
5491 "info" | "get_info" | "info_with_subjects" | "consumer_info" | "create_consumer"
5492 | "create_consumer_strict" | "update_consumer" | "get_consumer" | "get_or_create_consumer"
5493 | "delete_consumer" | "pause_consumer" | "resume_consumer" | "reset_consumer"
5494 // `unpin_consumer` (async-nats 0.52.0, feature `server_2_11`; the 0.50.0 lane found it absent): one
5495 // `CONSUMER.UNPIN.<stream>.<name>` request/reply through `Context::request`. EXECUTED against nats
5496 // `-js` behind a counting proxy: 106 up / 198 down (the stream existed, the consumer did not: the
5497 // request still goes out); an idle window and a pure control 0.
5498 | "unpin_consumer"
5499 | "delete_message" | "purge_subject" | "direct_get" | "direct_get_first_for_subject"
5500 | "direct_get_last_for_subject" | "direct_get_next_for_subject" | "get_raw_message"
5501 | "get_first_raw_message_by_subject" | "get_next_raw_message_by_subject"
5502 | "get_last_raw_message_by_subject" | "send" | "fetch"
5503 )
5504 }
5505 "Store" => {
5506 module == "jetstream::kv"
5507 && matches!(
5508 method,
5509 "create" | "create_with_ttl" | "put" | "entry" | "entry_for_revision" | "get" | "update"
5510 | "delete" | "delete_expect_revision" | "purge" | "purge_with_ttl"
5511 | "purge_expect_revision" | "purge_expect_revision_with_ttl" | "history" | "keys"
5512 | "watch" | "watch_many" | "watch_from_revision" | "watch_with_history"
5513 | "watch_many_with_history" | "watch_all" | "watch_all_from_revision"
5514 )
5515 }
5516 "ObjectStore" => {
5517 module == "jetstream::object_store"
5518 && matches!(
5519 method,
5520 "get" | "delete" | "info" | "put" | "watch" | "watch_with_history" | "list" | "seal"
5521 | "update_metadata" | "add_link" | "add_bucket_link"
5522 )
5523 }
5524 // `Consumer<pull::Config>` and its aliases; `info`/`get_info`/`reset` ask the broker, `messages` subscribes
5525 // (pull: inbox + a pull request, push: the deliver subject). `stream`/`fetch`/`batch` only build a request.
5526 "Consumer" | "PullConsumer" | "PushConsumer" | "OrderedPullConsumer" | "OrderedPushConsumer" => {
5527 matches!(module, "jetstream::consumer" | "jetstream::consumer::pull" | "jetstream::consumer::push")
5528 && matches!(method, "info" | "get_info" | "reset" | "messages")
5529 }
5530 "StreamBuilder" | "FetchBuilder" | "BatchBuilder" => {
5531 module == "jetstream::consumer::pull" && method == "messages"
5532 }
5533 "Message" | "Acker" => {
5534 js("jetstream::message") && matches!(method, "ack" | "ack_with" | "double_ack" | "double_ack_with")
5535 }
5536 // `ServiceExt::add_service` and `ServiceBuilder::start` (a builder chain reports `Client::start`),
5537 // `Service::endpoint`, `Group::endpoint`, `EndpointBuilder::add` (a chain reports `Service::add`).
5538 "Client" if matches!(module, "" | "client" | "service") => {
5539 return match method {
5540 "add_service" | "start" | "send_request" => Some("Net"),
5541 "new_inbox" => Some("Rand"),
5542 _ => None,
5543 };
5544 }
5545 "ServiceBuilder" => module == "service" && method == "start",
5546 "Service" | "Group" | "EndpointBuilder" => {
5547 module == "service" && matches!(method, "endpoint" | "add")
5548 }
5549 _ => false,
5550 };
5551 if !hit {
5552 return None;
5553 }
5554 Some("Net")
5555}
5556
5557/// SOUNDNESS R1528 — rdkafka 0.39.0, EXECUTED against a real Kafka broker (docker `apache/kafka`, advertised through
5558/// a counting TCP proxy: connections / bytes up / bytes down read around ONE call, an idle window of 0 read before it
5559/// on the same live client; handoff `rust-r1528/kafka/`). The suffix list in `classify` names `send`/`poll`/`flush`/
5560/// `subscribe`/`commit`/`fetch_*`; these are the verbs that list misses. Matching is on the receiver TYPE NAME plus
5561/// the method, with the module taken from the consumer's own spelling (crate root, the re-exporting module, or the
5562/// declaring module), because the method names are generic.
5563///
5564/// CHARGED `Net` (each moved the proxy's byte counters; every one of them with a quiet window beside it):
5565/// * every constructor that builds a librdkafka client (`ClientConfig::{create, create_with_context}`,
5566/// `Client::new`, and `from_config` / `from_config_and_context` of `AdminClient`, `BaseConsumer`,
5567/// `StreamConsumer`, `BaseProducer`, `ThreadedProducer`, `FutureProducer`): `rd_kafka_new` starts the broker
5568/// threads, which dial the bootstrap address on their own. EXECUTED: 1 to 4 connections and 67 to 372 bytes up
5569/// inside five seconds with NO further call, on 17 of 18 constructors in each of two runs (the 18th reads 0 once
5570/// and 2 on the repeat, a race with the previous client's teardown); a control pointed at a dead address moved
5571/// nothing through the proxy. `MockCluster::new` binds a loopback listener (EXECUTED: its bootstrap address
5572/// accepts a TCP connection), the same `Net` as a `TcpListener::bind`.
5573/// * `AdminClient::{create_topics, delete_topics, create_partitions, delete_groups, delete_records,
5574/// describe_configs, alter_configs}`: one admin request each (37 to 186 bytes up, 26 to 2,168 down).
5575/// * `{Base,Stream}Consumer::{committed, offsets_for_timestamp}` (an OffsetFetch / ListOffsets round trip, EXECUTED
5576/// with an assigned partition: 47 to 56 bytes up), `incremental_assign` (EXECUTED 391 to 587 bytes up: the
5577/// partition's leader is looked up and the fetcher starts) and `seek_partitions` (EXECUTED 51 bytes up when the
5578/// target is a logical offset on a live group partition, 0 on a manual one). Conditional on the state the caller
5579/// is in, exactly like the existing `::seek` rule beside it, and charged for the same reason: a `Net` that is
5580/// sometimes absent is an over-approximation, a silent one is not.
5581/// * `{Base,Threaded,Future}Producer::{init_transactions, commit_transaction, abort_transaction,
5582/// send_offsets_to_transaction}`: the transaction coordinator round trips.
5583///
5584/// CHARGED `Clock`: `Timestamp::now` and `util::current_time_millis` (EXECUTED: two reads five milliseconds apart
5585/// differ).
5586///
5587/// NOT claimed, and why (each EXECUTED to move no byte on a quiet client): `assignment`, `subscription`, `position`
5588/// (local state), `incremental_unassign`, `begin_transaction` (a local state change). The Rust bodies of `TopicPartitionList::{new,
5589/// with_capacity, from_topic_map}`, `OwnedHeaders::{new, new_with_capacity}`, `{Borrowed,}Headers::detach`,
5590/// `BorrowedMessage::detach` and `ClientConfig::create_native_config` do no I/O: their only effect is the `trace!`
5591/// that `NativePtr::drop` emits (R1582, `OPEN_ENTRY_VERDICTS`).
5592///
5593/// One effect is all `classify` can carry: every constructor also emits `trace!`/`debug!` events (`Log`, EXECUTED
5594/// through a Trace-level `log` logger: 1 to 46 records per call) and the transaction verbs a `flush` (`Clock`); neither
5595/// is charged (the one-effect limit R1575 records for the same shape).
5596fn rdkafka_r1528(path: &str) -> Option<&'static str> {
5597 let rest = path.strip_prefix("rdkafka::")?;
5598 let (head, method) = rest.rsplit_once("::")?;
5599 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5600 let from_config = matches!(method, "from_config" | "from_config_and_context");
5601 let txn = matches!(method, "init_transactions" | "commit_transaction" | "abort_transaction" | "send_offsets_to_transaction");
5602 let consumer_net = matches!(method, "committed" | "offsets_for_timestamp" | "incremental_assign" | "seek_partitions");
5603 let hit = match ty {
5604 "ClientConfig" => matches!(module, "" | "config") && matches!(method, "create" | "create_with_context"),
5605 "Client" => matches!(module, "" | "client") && method == "new",
5606 "MockCluster" => matches!(module, "" | "mocking") && method == "new",
5607 "AdminClient" => {
5608 matches!(module, "" | "admin")
5609 && (from_config
5610 || matches!(
5611 method,
5612 "create_topics" | "delete_topics" | "create_partitions" | "delete_groups" | "delete_records"
5613 | "describe_configs" | "alter_configs"
5614 ))
5615 }
5616 "BaseConsumer" => {
5617 matches!(module, "" | "consumer" | "consumer::base_consumer") && (from_config || consumer_net)
5618 }
5619 "StreamConsumer" => {
5620 matches!(module, "" | "consumer" | "consumer::stream_consumer") && (from_config || consumer_net)
5621 }
5622 "BaseProducer" | "ThreadedProducer" => {
5623 matches!(module, "" | "producer" | "producer::base_producer") && (from_config || txn)
5624 }
5625 "FutureProducer" => {
5626 matches!(module, "" | "producer" | "producer::future_producer") && (from_config || txn)
5627 }
5628 "Timestamp" => {
5629 return (matches!(module, "" | "message") && method == "now").then_some("Clock");
5630 }
5631 // `util::current_time_millis` (a free fn: `head` is its module, the "type" is the module name).
5632 "util" => {
5633 return (module.is_empty() && method == "current_time_millis").then_some("Clock");
5634 }
5635 _ => false,
5636 };
5637 hit.then_some("Net")
5638}
5639
5640/// SOUNDNESS R1528 — mongodb 3.9.1, EXECUTED against a real mongod (docker `mongo`, replica set, reached through a
5641/// counting TCP proxy; the driver's own connection only, `heartbeatFrequencyMS` one hour so the idle window reads 0;
5642/// the server's `serverStatus` opcounters / `transactions` read beside it; handoff `rust-r1528/dbs/`). The macro
5643/// `#[action_impl]` turns every `impl Action for X { async fn execute }` into `IntoFuture::into_future` (and a sync
5644/// `run`), so `X::execute` is a spelling NO consumer can write; what a consumer writes is the CREATOR
5645/// (`coll.drop()`, `session.commit_transaction()`), whose return value it awaits. The existing rule above already
5646/// charges the creators it lists (`find`, `count_documents`, ...). These are the creators it misses, charged `Db`,
5647/// plus the `X::execute` / `X::batch` spellings the coverage generator guesses (a phantom spelling, recognised so the
5648/// row is not a standing alarm; the effect it names is the creator's).
5649///
5650/// CHARGED `Db` (each moved the proxy's byte counters, with a quiet window beside it; the sync client's variants
5651/// share the async body through `TOKIO_RUNTIME.block_on` and were run too):
5652/// * `ClientSession::{commit_transaction, abort_transaction}`: 283 / 282 bytes up WHEN THE TRANSACTION HOLDS AN
5653/// OPERATION (server `transactions.totalCommitted` / `totalAborted` +1); an EMPTY transaction sends nothing (0 up),
5654/// so the charge is state-conditional, kept for the reason `rdkafka::seek` is.
5655/// * `StartTransaction::{and_run, and_run2}` (reached as `ClientSession::and_run`): 675 bytes up with a callback
5656/// that inserts, against 392 for the same insert in a manual transaction with no commit, so the difference is
5657/// the `commitTransaction`; a callback that does nothing sends 0.
5658/// * `Client::{bulk_write, shutdown}` (384 up; `endSessions` 351 up), `Database::{drop, run_cursor_command}`,
5659/// `Collection::{drop, drop_index, drop_indexes, list_indexes, list_index_names}`.
5660/// * `Collection::{create_search_index, create_search_indexes}`: the request goes out (284 up, 505 down) and a
5661/// community mongod answers `SearchNotEnabled` (31082); on Atlas it succeeds. WEAKER: not run to success.
5662/// * `ChangeStream::next_if_any`, `SessionChangeStream::{next, next_if_any}`: the FIRST call returns the initial
5663/// batch with no I/O and every later call is a `getMore` (264 up, 292 to 845 down); state-conditional.
5664/// * `GridFsBucket::delete` (677 up), `GridFsUploadStream::abort` (332 up).
5665///
5666/// CHARGED `Clock`: `oidc::Callback::{human, machine}` build a `Cache` whose `last_call_time` is `Instant::now()`
5667/// (client/auth/oidc.rs:343). WEAKER: read, not observable from outside the process.
5668///
5669/// NOT claimed: `ClientSession::start_transaction` alone (EXECUTED 0 bytes), `ClientOptions::parse` and
5670/// `ConnectionString::parse` (R1584), the `in-use-encryption` surface (R1585).
5671fn mongodb_r1528(path: &str) -> Option<&'static str> {
5672 let rest = path.strip_prefix("mongodb::")?;
5673 let (head, method) = rest.rsplit_once("::")?;
5674 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5675 // A consumer's spelling: the crate root, the `sync` re-export, or the declaring `action::..` module.
5676 let consumer = module.is_empty() || module == "sync" || module.starts_with("action") || module.starts_with("sync::");
5677 let hit = match (ty, method) {
5678 ("ClientSession", "commit_transaction" | "abort_transaction" | "and_run" | "and_run2") => consumer,
5679 ("Client", "shutdown" | "bulk_write") => consumer,
5680 ("Database", "drop" | "run_cursor_command") => consumer,
5681 ("Collection", "drop" | "drop_index" | "drop_indexes" | "list_indexes" | "list_index_names"
5682 | "create_search_index" | "create_search_indexes") => consumer,
5683 ("ChangeStream", "next_if_any") => {
5684 matches!(module, "" | "sync" | "change_stream" | "sync::change_stream")
5685 }
5686 ("SessionChangeStream", "next" | "next_if_any") => {
5687 matches!(module, "" | "sync" | "change_stream" | "change_stream::session" | "sync::change_stream")
5688 }
5689 ("GridFsBucket", "delete") | ("GridFsUploadStream", "abort") => {
5690 matches!(module, "" | "sync" | "gridfs" | "sync::gridfs")
5691 }
5692 // The macro-input / action-type spellings (no consumer can write them; see above).
5693 ("StartTransaction", "and_run" | "and_run2") => true,
5694 // `Aggregate` (SOUNDNESS R1528, mongodb 3.10.0 refresh): EXECUTED against mongo:latest behind a counting proxy after a
5695 // warm-up op: `.await`, `.batch().await`, and both with `.session(..)` each moved 139 up / 92 down; the builder
5696 // `aggregate(..)` held without awaiting, and an idle window, 0.
5697 ("Aggregate" | "AbortTransaction" | "CommitTransaction" | "BulkWrite" | "CountDocuments" | "CreateCollection"
5698 | "CreateSearchIndex" | "DropCollection" | "DropDatabase" | "DropIndex" | "Find" | "FindOneAndDelete"
5699 | "FindOneAndReplace" | "FindOneAndUpdate" | "ListCollections" | "ListDatabases" | "ListIndexes"
5700 | "RunCommand" | "RunCursorCommand" | "Shutdown" | "Watch", "execute" | "batch") => true,
5701 // `Delete` is also the collection delete action; only the GridFS one is a row here.
5702 ("Delete", "execute") => module.contains("gridfs"),
5703 ("Callback", "human" | "machine") => {
5704 return matches!(module, "" | "options::oidc" | "client::auth::oidc").then_some("Clock");
5705 }
5706 _ => false,
5707 };
5708 hit.then_some("Db")
5709}
5710
5711/// SOUNDNESS R1528 — mysql_async 0.37.1, EXECUTED against a real mysqld (docker `mysql:8.4`, binary log on, reached
5712/// through a counting TCP proxy; the server's `Com_*` status beside it; handoff `rust-r1528/dbs/`). Matching is on the
5713/// receiver TYPE NAME plus the method, because the method names are generic.
5714///
5715/// CHARGED `Db` (each moved the proxy's byte counters, or a server `Com_*` counter, with a quiet window beside it):
5716/// * `Conn::{change_user, reset, disconnect, get_binlog_stream, close, start_transaction}`, `BinlogStream::close`,
5717/// `Transaction::{close, commit, rollback}`, `Connection::close`: `close(stmt)` is `COM_STMT_CLOSE` (9 up, 0 down,
5718/// `Com_stmt_close` +1); `commit` / `rollback` move `Com_commit` / `Com_rollback`; `Conn::start_transaction` is
5719/// not a coverage-gate row (the generator's self-scan gave it no concrete effect) but is the same `BEGIN` round trip
5720/// as the `Pool::start_transaction` row, so it is charged with it.
5721/// * `Pool::{start_transaction, to_connection}`: a NEW connection (1 connection, 160 to 195 bytes up). The effect is
5722/// in the future `to_connection()` returns and a consumer drives with `.resolve().await`; EXECUTED: the same call NOT
5723/// resolved moves 0 bytes. Charged at the creator, the only attributed path, like `get_conn`.
5724/// * `QueryResult::{next, collect, try_collect, *_and_drop, for_each, reduce, drop_result}`: socket reads of the
5725/// result set. The proxy counts what the SERVER sends, which stalls on a full client buffer, so a 30 MB result held
5726/// idle reads 0 and each of these then moved ~29.6 MB down; `next` x5000 moved 6.4 MB. `stream` / `stream_and_drop`
5727/// moved 0 at creation and 5.2 MB when the returned stream was polled 5000 times: charged at the creator (WEAKER).
5728/// Reached only where the receiver is TYPED (a parameter or annotation): a result held in an untyped `let` carries
5729/// the `query_iter` / `exec_iter` that produced it, which is already `Db`.
5730/// * `QueryWithParams::{run, batch, first, fetch, ignore, reduce, stream}`, `prelude::Query::{..}`,
5731/// `prelude::BatchQuery::batch` and `Q::run` (the generator's spelling of `impl<Q: AsQuery> Query for Q`): the
5732/// query goes out (13 to 81 bytes up, `Com_stmt_execute`). EXECUTED for `run`, `first` and `batch` on a typed
5733/// `QueryWithParams` and for `run` / `fetch` / `ignore` through a `T: Query` bound; `reduce` and `stream`
5734/// share the default-method body (`self.run(conn).await?...`): WEAKER, read. ONLY the typed and bound spellings
5735/// reach this rule: `"SELECT 1".run(conn)` and `"..".with(p).run(conn)` carry no mysql_async path at all (the trace
5736/// shows an unattributed `run`), EXECUTED to send the query and filed as R1586.
5737///
5738/// CHARGED `Env`: `test_misc::{get_opts, test_compression, test_ssl}` read `DATABASE_URL`, `COMPRESS`, `SSL`
5739/// (lib.rs:621-678); a `doc(hidden)` public module.
5740///
5741/// NOT claimed, and why: `QueryResult::map` and the `map` of `QueryWithParams` / `Query` (R1586: the scan attributes
5742/// EVERY `.map(..)` hanging off a typed root to that root's type, so a rule on the bare name charges `Db` to the pure
5743/// `r.columns().map(..)`; MEASURED on `c_qr_cols_first`, the trace shows `mysql_async::QueryResult::map` for it, and
5744/// the same trace shows `Conn::map`, `Pool::iter`, `Transaction::map` for the other accessors), `StatementLike::
5745/// to_statement` (read and pure: `REVIEWED_PURE_ENTRIES`), `PathOrBuf::read` (R1586), `Pool::get_conn` /
5746/// `Conn::new` / `Conn::from_url` (already `Db` above).
5747fn mysql_async_r1528(path: &str) -> Option<&'static str> {
5748 let rest = path.strip_prefix("mysql_async::")?;
5749 let (head, method) = rest.rsplit_once("::")?;
5750 let (module, ty) = head.rsplit_once("::").unwrap_or(("", head));
5751 if ty == "test_misc" && module.is_empty() {
5752 return matches!(method, "get_opts" | "test_compression" | "test_ssl").then_some("Env");
5753 }
5754 let hit = match ty {
5755 "Conn" => {
5756 matches!(module, "" | "conn" | "conn::binlog_stream" | "queryable" | "queryable::transaction")
5757 && matches!(method, "change_user" | "reset" | "disconnect" | "get_binlog_stream" | "close" | "start_transaction")
5758 }
5759 "BinlogStream" => matches!(module, "" | "conn::binlog_stream") && method == "close",
5760 "Connection" => matches!(module, "" | "queryable") && method == "close",
5761 "Transaction" => {
5762 matches!(module, "" | "queryable" | "queryable::transaction") && matches!(method, "close" | "commit" | "rollback")
5763 }
5764 "Pool" => {
5765 matches!(module, "" | "conn::pool" | "connection_like") && matches!(method, "start_transaction" | "to_connection")
5766 }
5767 "QueryResult" => {
5768 matches!(module, "" | "queryable::query_result" | "queryable::query_result::result_set_stream")
5769 && matches!(
5770 method,
5771 "next" | "collect" | "try_collect" | "collect_and_drop" | "try_collect_and_drop" | "for_each"
5772 | "for_each_and_drop" | "map_and_drop" | "reduce" | "reduce_and_drop" | "drop_result"
5773 | "stream" | "stream_and_drop"
5774 )
5775 }
5776 "QueryWithParams" => {
5777 matches!(module, "" | "query")
5778 && matches!(method, "run" | "batch" | "first" | "fetch" | "ignore" | "reduce" | "stream")
5779 }
5780 "Query" => {
5781 module == "prelude" && matches!(method, "run" | "first" | "fetch" | "ignore" | "reduce" | "stream")
5782 }
5783 "BatchQuery" => module == "prelude" && method == "batch",
5784 "Q" => matches!(module, "" | "query") && method == "run",
5785 _ => false,
5786 };
5787 hit.then_some("Db")
5788}
5789
5790pub const PG_DB_VERBS: &[&str] = &[
5791 "::query", "::query_one", "::query_opt", "::query_raw", "::execute",
5792 "::batch_execute", "::simple_query", "::prepare", "::prepare_typed",
5793 "::copy_in", "::copy_out", "::transaction", "::connect",
5794 // `Config::connect_raw` (tokio-postgres config.rs:739) does the SAME protocol handshake
5795 // as `Config::connect` over a caller-supplied stream (a Unix socket, a TLS-terminated
5796 // proxy) instead of dialing one itself — real Db/Net I/O, missing because the verb
5797 // doesn't end in the plain `connect` spelling.
5798 "::connect_raw",
5799 // rusqlite's dialect of the same verbs (a verb-probe found the CANONICAL rusqlite
5800 // consumer API classifying pure): `query_row` is the one-row read, `query_map`/
5801 // `query_and_then` the many-row reads, `execute_batch` is rusqlite's name for
5802 // batch_execute, `prepare_cached` round-trips like prepare. `query_typed` is
5803 // tokio_postgres 0.7.10+.
5804 "::query_row", "::query_map", "::query_and_then", "::execute_batch",
5805 "::prepare_cached", "::query_typed",
5806 "::prepare_typed_cached",
5807 // SOUNDNESS R1528 — rusqlite 0.39 `Connection::query_row_and_then(sql, params, f)` (lib.rs:741)
5808 // is `prepare(sql)` + `query` + `get_expected_row`, and `prepare_with_flags(sql, flags)`
5809 // (lib.rs:783) is `InnerConnection::prepare` -> `sqlite3_prepare_v3`: the same one-statement
5810 // round-trips as `query_row`/`prepare` above, with the SQL text at argument 0, so they join
5811 // the table the masking guard reads. No other table crate has either name.
5812 "::query_row_and_then", "::prepare_with_flags",
5813];
5814
5815/// The members of `PG_DB_VERBS` that take NO SQL text, each checked against its signature:
5816/// `Client::transaction(&mut self)`, `connect(params, tls)` / `Config::connect_raw(stream, tls)` —
5817/// a connection string or a stream, never a query. A name wrongly listed here is a silent bypass;
5818/// a name missing from here is an over-mask that fails closed. SOUNDNESS R806.
5819pub const PG_DB_VERBS_NO_SQL: &[&str] = &["::transaction", "::connect", "::connect_raw"];
5820
5821/// The members of `LIBC_FS` that take NO path — every one operates on a file DESCRIPTOR, a `DIR*`
5822/// stream, or nothing — so a missing literal there hides no locator. **CHECKED AGAINST EACH SIGNATURE,
5823/// because this is a denylist on a masking guard: a path-taking name wrongly listed here is the
5824/// benign-sibling-literal bypass (R399), while a no-path name missing from here only over-masks.**
5825/// Everything else in `LIBC_FS` names a path, at argument 0 or — for the `*at` family — argument 1.
5826pub const LIBC_FS_NO_PATH: &[&str] = &[
5827 "ftruncate", "ftruncate64", "fdopendir", "readdir", "readdir64", "readdir_r", "closedir",
5828 "rewinddir", "seekdir", "telldir", "fchdir", "getcwd", "get_current_dir_name", "fstatfs",
5829 "fstatfs64", "fstatvfs", "fsync", "fdatasync", "sync", "syncfs", "sync_file_range", "fallocate",
5830 "posix_fallocate", "posix_fadvise", "sendfile", "sendfile64", "copy_file_range", "flock",
5831 "getdents", "getdents64", "futimens",
5832];
5833
5834/// The members of `LIBC_EXEC` that name NO program: process duplication (`fork`/`vfork`/`clone*`,
5835/// which run THIS image) and reaping (`wait*`, `pclose`). Every other member names what runs —
5836/// `system`/`popen` a shell command line, `exec*`/`posix_spawn*` a path or file name, `fexecve` an fd
5837/// whose program is invisible at the call. Same direction rule as `LIBC_FS_NO_PATH`.
5838pub const LIBC_EXEC_NO_PROGRAM: &[&str] = &[
5839 "fork", "vfork", "clone", "clone3", "wait", "waitpid", "wait3", "wait4", "waitid", "pclose",
5840];
5841
5842/// The members of `LIBC_NET` that name NO destination: socket setup and options, the LOCAL side
5843/// (`bind`/`listen`/`accept*` — where the process listens, never a place it reaches; ⟨0.29⟩
5844/// `is_net_binding`), use-verbs on an already-connected or already-bound socket (`send`/`recv*`),
5845/// teardown and read-backs. The destination-naming remainder is `connect`, `sendto`, `sendmsg`,
5846/// `sendmmsg`, and the resolver pair `getaddrinfo`/`getnameinfo`.
5847pub const LIBC_NET_NO_DESTINATION: &[&str] = &[
5848 "socket", "setsockopt", "getsockopt", "bind", "listen", "accept", "accept4", "shutdown", "send",
5849 "recv", "recvfrom", "recvmsg", "recvmmsg", "getpeername", "getsockname", "freeaddrinfo",
5850];
5851
5852/// Whether `crate_name` is one of the three crates `classify` routes through the libc syscall tables.
5853pub fn is_syscall_crate(crate_name: &str) -> bool {
5854 matches!(crate_name, "libc" | "nix" | "rustix")
5855}
5856
5857/// Whether a subprocess-builder method only MODIFIES the command (`.arg`, `.env`, `.current_dir`)
5858/// rather than NAMING the program (`Command::new`, `duct::cmd`). A WHOLE-CRATE-Exec crate
5859/// (`portable_pty`, `duct`, `async_process`) classifies *every* method as `Exec`, so the
5860/// head-refinement must skip these: an arg or env-var-name literal that happened to match a head
5861/// (`.env("psql", …)`, `.arg("curl")`) would FABRICATE that effect — the §1 under-report rule. The
5862/// method is the call path's last segment.
5863pub fn is_cmd_builder_method(method: &str) -> bool {
5864 matches!(
5865 method,
5866 "arg" | "args" | "arg0" | "env" | "envs" | "env_clear" | "env_remove" | "current_dir"
5867 | "cwd" | "stdin" | "stdout" | "stderr" | "pre_exec" | "creation_flags" | "uid" | "gid"
5868 | "groups" | "process_group"
5869 )
5870}
5871
5872/// Whether a subprocess method NAMES the program (so its first string literal IS the command head to
5873/// refine): `Command::new("curl")`, `duct::cmd("curl", …)`. The head-refinement must fire ONLY here —
5874/// an ALLOWLIST, not "any method except known modifiers". A whole-crate-Exec crate classifies EVERY
5875/// method as `Exec`, so a denylist leaked NON-naming methods that aren't modifiers — a getter like
5876/// `CommandBuilder::get_env("psql")` (reading back an env-var KEY, not a program) fed `"psql"` to the
5877/// head classifier and FABRICATED `Db` (review find). Only `new`/`cmd` name a program; everything else
5878/// (modifiers, getters `get_*`, custom builder methods) keeps the bare `Exec` cliff — under-refine
5879/// (safe) rather than fabricate. `std::process::Command` is verb-precise so getters never fire `Exec`
5880/// there anyway; the allowlist makes the whole-crate-Exec crates safe too.
5881pub fn is_cmd_naming_method(method: &str) -> bool {
5882 matches!(method, "new" | "cmd")
5883}
5884
5885/// The masking guard (AS-EFF-008): a Net call whose method takes the HOST/URL as an argument is
5886/// "establishing" — a classified Net call here with no captured host literal leaves the endpoint
5887/// structurally INVISIBLE (a runtime-built host), so the surface is incomplete and the gate must fail
5888/// closed (else a benign sibling literal masks the runtime endpoint). An ALLOWLIST of connection-
5889/// establishing verbs — the SAFE direction: a USE-verb on an already-connected socket
5890/// (`stream.write`/`read`/`flush`, `socket.send`/`recv`) is NOT here, so a missing literal there (the
5891/// host was fixed at `connect`) never false-positives. **THAT LAST CLAIM WAS FALSE AND IS WITHDRAWN —
5892/// see R379 in the body: under-catching an establishing verb IS a broken gate whenever a benign sibling
5893/// literal sits in the same function, which is the exact scenario the first sentence names.** The list is
5894/// now an ALLOWLIST still — see the R379 body below: the denylist inversion was BUILT, A/B'd over
5895/// 1,545 crates and REFUSED (544 rows over-masked). An earlier draft of this line said "now a
5896/// DENYLIST", which was a leftover from the rejected cut and asserted the opposite of the code it
5897/// sits on — the comment-asserting-safety shape this register records. The arg is the method (path's
5898/// last segment).
5899pub fn is_net_establishing(method: &str) -> bool {
5900 // SOUNDNESS R379 — the DNS RESOLVER verbs were missing, and their absence was a GATE BYPASS.
5901 //
5902 // `classify` (see the `hickory_resolver` arm) already maps `::lookup_ip`, `::lookup` and any
5903 // `*_lookup` to `Net`, and asserts it. This list did not, so a `Net` call whose host is a runtime
5904 // value was never marked `incomplete` and a benign sibling literal certified it. Measured against
5905 // real hickory-resolver 0.26.2, on a fixture that builds clean:
5906 //
5907 // pub async fn masked(r: &Resolver<TokioRuntimeProvider>, host: &str) {
5908 // let _ = r.lookup_ip("good.example.com").await; // the benign literal
5909 // let _ = r.lookup_ip(host).await; // the runtime target
5910 // }
5911 //
5912 // reported `hosts:['good.example.com'] incomplete:NONE`, and `allow Net good.example.com` exited
5913 // **0** over a DNS resolution of a caller-controlled hostname. `TcpStream::connect(host)` — one
5914 // verb over, same fixture — marked `incomplete:['Net']` and failed closed. The mechanism was
5915 // sound; only the enumeration was short.
5916 //
5917 // THE DOC ABOVE USED TO CLAIM under-catching here is "a missed mask (sound-with-disclosure), never
5918 // a broken gate". That is false and is withdrawn: it is a broken gate whenever a benign sibling
5919 // literal shares the function, which is the scenario the guard's own first sentence names.
5920 //
5921 // **AND IT STAYS AN ALLOWLIST, WHICH IS NOT THE FAMILY'S USUAL DIRECTION — the exception is
5922 // measured and it is the same one `is_cmd_naming_method` states two doc-blocks up.** Inverting to
5923 // a denylist of use-verbs was built and A/B'd over all 1,545 registry crates: **544 rows gained
5924 // `incomplete` and none lost it**, because a WHOLE-CRATE-Net crate classifies EVERY method as Net,
5925 // so a denylist masks essentially everything in one — `cap-std` 66 rows, `hickory-net` 44. The
5926 // triggers it added were conversions, accessors and combinators (`into` 24, `from_std` 20,
5927 // `as_socket` 15, `setsockopt` 32, `map_err` 7, `ok` 5, `try_clone` 4) — none of which hides a
5928 // host. Worse, it masked **`bind` (31)**, which this very file has `is_net_binding` to exclude
5929 // because a bind address is where the process LISTENS, never a destination it reaches. So here the
5930 // allowlist is the precise side and the denylist is the leaky one, exactly as recorded for Exec.
5931 //
5932 // The resolver family is written out rather than the one spelling in hand (R346): `_lookup`
5933 // catches `reverse_lookup`/`srv_lookup`/`mx_lookup`/`txt_lookup`/`ipv4_lookup`/`ipv6_lookup` and
5934 // the rest of the record-type verbs in one test, mirroring `classify`'s own `ends_with("_lookup")`.
5935 method.ends_with("_lookup")
5936 || matches!(
5937 method,
5938 "connect"
5939 | "connect_timeout"
5940 | "get"
5941 | "post"
5942 | "put"
5943 | "patch"
5944 | "delete"
5945 | "head"
5946 | "request"
5947 | "send_to"
5948 | "lookup_host"
5949 | "to_socket_addrs"
5950 // R379 — the DNS resolver verbs `classify` already calls Net.
5951 | "lookup"
5952 | "lookup_ip"
5953 | "resolve"
5954 )
5955}
5956
5957/// ⟨0.29⟩ The Net verbs whose LOCATOR is at argument **1**, not 0 — the rust analogue of candor-ts's
5958/// `NET_URL_ARG1_MEMBERS`.
5959///
5960/// **A REGRESSION FOUND IN REVIEW, and the direction matters.** The positional-literal rung replaced
5961/// `first_str_lit` ("the first string literal ANYWHERE in the call") with `positional_str_lit(args, 0)`
5962/// as the UNIVERSAL default. That is right for `Fs`/`Db`/`Exec`, whose locator is always argument 0 —
5963/// but `is_net_establishing` already listed two verbs whose locator is not: `reqwest::Client::request`
5964/// takes `(Method, url)` and `UdpSocket::send_to` takes `(buf, addr)`. MEASURED after the swap:
5965/// `c.request(Method::GET, "https://api.example.com/v1")` published NO `hosts` at all and could not be
5966/// certified, while `c.get("https://api.example.com/v1")` certified normally.
5967///
5968/// ⟨0.29⟩ MEASURED FOR `request` ONLY. `send_to` is a METHOD on a receiver the STABLE syntactic backend
5969/// does not type, so `s.send_to(buf, "203.0.113.9:53")` is not classified at all there and its arg-1
5970/// literal is still uncaptured on the floor; the position is right and the deep engine can use it. Said
5971/// plainly because the first version of this fix's changelog listed both verbs as measured.
5972///
5973/// The direction is SAFE — an uncaptured locator fails closed, it never certifies something invisible —
5974/// which is exactly why it needed a review to find: the gate stayed sound and quietly stopped being
5975/// USABLE for a common shape. candor-ts avoided this by making its resolver verb-aware in the same rung;
5976/// this is that discipline arriving one engine late.
5977pub fn is_net_host_arg1(method: &str) -> bool {
5978 matches!(method, "request" | "send_to")
5979}
5980
5981/// ⟨0.29⟩ / ⟨0.40⟩ A LOCAL BIND/LISTEN VERB — the address it names is where the process LISTENS, never a
5982/// destination it reaches (SPEC §2 ⟨0.40⟩ *A BIND OR LISTEN ADDRESS IS WHERE THE PROCESS LISTENS*, PART 96
5983/// `a_litbind`). Its literal never enters `hosts`.
5984///
5985/// **MEASURED, and it is a false all-clear rather than a naming quibble.** `UdpSocket::bind("0.0.0.0:0")`
5986/// put `0.0.0.0:0` into `hosts`, the DESTINATION surface `allow Net` gates on (§2), and — because a
5987/// literal had been captured — nothing marked the surface incomplete. So:
5988///
5989/// ```text
5990/// let s = UdpSocket::bind("0.0.0.0:0")?; // local
5991/// s.send_to(b"secrets", dst); // destination is a RUNTIME value
5992/// allow Net 0.0.0.0 -> policy ✓, exit 0
5993/// ```
5994///
5995/// A local listen address certified a send to an endpoint nobody can see. That is the masking evasion
5996/// AS-EFF-008 exists to close, reached through a verb whose literal is not a destination at all.
5997///
5998/// **A BIND MARKS NOTHING** (⟨0.40⟩). This doc used to say *"A BIND CANNOT BE CERTIFIED, EVER, which is
5999/// why this marks the surface incomplete"*, and that has been false since `16fb46f` dropped the hedge:
6000/// neither rust engine marks a bind, and both only withhold its literal. ⟨0.40⟩ makes the withholding the
6001/// rule — an ephemeral client's own `send_to`/`connect` carries its locator under `is_net_establishing`,
6002/// so hedging the bind as well made every UDP client uncertifiable while adding nothing a gate could use;
6003/// and a bind with nothing beside it still fails `allow Net` closed on its EMPTY literal surface. The
6004/// half of the old sentence that WAS right — *a server that binds and accepts talks to whoever connects*
6005/// — belongs to the ACCEPT, not the bind, and lives in `is_net_accepting`, which is why `accept` and
6006/// `incoming` are no longer members here.
6007pub fn is_net_binding(method: &str) -> bool {
6008 matches!(method, "bind" | "listen" | "bind_to_device")
6009}
6010
6011/// ⟨0.40⟩ A CALL THAT ACCEPTS CONNECTIONS — its peers are chosen by whoever connects, so no literal can name
6012/// them, and the unit's `Net` surface is INCOMPLETE (SPEC §2 ⟨0.40⟩ *A UNIT THAT ACCEPTS A CONNECTION …*,
6013/// AS-EFF-008, SEMANTICS §6 `masked_Net`; PART 96 `c_accept`). Both rust engines mark `incomplete` on a
6014/// `Net`-classified call this answers, WHETHER OR NOT a literal was captured (a server bootstrap's literal is
6015/// a LISTEN address, withheld like a bind's), and contribute no `hosts` entry for it.
6016///
6017/// **SOUNDNESS R817 — `accept`/`incoming` WERE MEMBERS OF `is_net_binding`, WHICH WITHHOLDS AND NEVER
6018/// MARKS**, so they were read exactly like a use-verb. `pub fn f(l: &TcpListener) { let _ =
6019/// TcpStream::connect("ok.example:80"); if let Ok((mut s, _)) = l.accept() { s.write_all(b"hi") } }`
6020/// reported `hosts: ["ok.example:80"]`, no `incomplete`, and `allow Net in f ok.example` exited **0** over a
6021/// server writing to whoever connected — EXECUTED: a client on 127.0.0.1 received the bytes.
6022///
6023/// The membership, by spelling (`path` is the resolved call path, `leaf` its last segment):
6024/// * `accept`, `accept4` (libc/nix/rustix), `poll_accept` (tokio's poll form), `incoming` and
6025/// `into_incoming` (std's iterator forms) — exact LEAF. `accept_bi`/`accept_uni` (quinn) are NOT here:
6026/// they accept a STREAM on a connection whose peer was already fixed by the endpoint's `accept`.
6027/// * tonic's `Router::serve`/`Router::serve_with_shutdown` and `TcpIncoming::new`, the server bootstraps
6028/// `classify` already charges `Net` — each binds a listener and begins accepting on it (classify's own
6029/// tonic arm cites the source lines). By PATH, because `serve`/`new` are not accept verbs anywhere else.
6030///
6031/// A wrong `true` over-masks and fails CLOSED, saying so; a wrong `false` is the benign-sibling-literal
6032/// bypass this row closed. An exact `accept` leaf also matches a TLS/WebSocket acceptor wrapping a stream
6033/// that was already accepted (`TlsAcceptor::accept(stream)`, `tungstenite::accept`) — whose peer is likewise
6034/// whoever connected, so the mark is redundant there, never false.
6035pub fn is_net_accepting(path: &str, leaf: &str) -> bool {
6036 matches!(leaf, "accept" | "accept4" | "poll_accept" | "incoming" | "into_incoming")
6037 || (path.starts_with("tonic::")
6038 && (path.ends_with("::Router::serve")
6039 || path.ends_with("::Router::serve_with_shutdown")
6040 || path.ends_with("::TcpIncoming::new")))
6041}
6042
6043/// The masking guard (AS-EFF-008), the `Fs` analog of `is_net_establishing`: whether an `Fs`-classified
6044/// call takes the filesystem PATH as a string argument (so a missing literal leaves the path
6045/// structurally INVISIBLE — a runtime-built path — and the surface is incomplete, fail-closed). An
6046/// ALLOWLIST of the path-NAMING free functions / constructors (`fs::write`/`read`/`File::open`/…), the
6047/// SAFE direction: a path-stat METHOD whose path is the RECEIVER (`p.metadata()`, `p.exists()`) is
6048/// invoked method-form and the caller gates on `!is_method`, so this never sees it; an op on an
6049/// already-opened handle (`file.write_all`, `mmap.flush`, `tempfile()` — a random name, no path arg)
6050/// is not here, so a missing literal there never false-positives. Under-catching an unusual
6051/// path-naming fn is a missed mask (sound-with-disclosure), ~~never a broken gate~~ — **AND THAT LAST
6052/// CLAUSE IS FALSE. SOUNDNESS R399.** R379 withdrew this exact sentence from `is_net_establishing` after
6053/// MEASURING it wrong, and R383 and R386 are counter-examples on this guard and its Db sibling: an
6054/// unlisted path-taking call contributes no locator, nothing marks the surface incomplete, and a benign
6055/// sibling literal then CERTIFIES the invisible destination — `allow Fs /etc/hostname` exit 0 over
6056/// `unixfs::chown(caller, …)`, with the control (`OpenOptions::open(caller)`) exiting 1. The sentence
6057/// survived here only because nobody re-read it after R379, and it is what licenses leaving the list
6058/// short. Under-catching is a BROKEN GATE, not a missed mask. The arg is the
6059/// method/fn leaf (the path's last segment).
6060pub fn is_fs_path_arg(leaf: &str) -> bool {
6061 matches!(
6062 leaf,
6063 // SOUNDNESS R399 — the POSIX ownership/root verbs take a path as argument 0 and were absent, so a
6064 // benign sibling literal certified them: `allow Fs /etc/hostname` exit 0 over
6065 // `unixfs::chown(caller, …)`, control `OpenOptions::open(caller)` exit 1. `std::os::unix::fs`.
6066 "chown"
6067 | "lchown"
6068 | "chroot"
6069 // std::fs / tokio::fs / async_std::fs / fs_err free functions taking a path argument
6070 | "write"
6071 | "read"
6072 | "read_to_string"
6073 | "read_dir"
6074 | "read_link"
6075 | "copy"
6076 | "rename"
6077 | "remove_file"
6078 | "remove_dir"
6079 | "remove_dir_all"
6080 | "create_dir"
6081 | "create_dir_all"
6082 | "hard_link"
6083 | "soft_link"
6084 | "symlink"
6085 | "symlink_file"
6086 | "symlink_dir"
6087 | "symlink_metadata"
6088 | "canonicalize"
6089 | "metadata"
6090 | "set_permissions"
6091 | "exists"
6092 | "try_exists"
6093 // File / OpenOptions constructors taking a path argument
6094 | "open"
6095 | "create"
6096 | "create_new"
6097 )
6098}
6099
6100/// SOUNDNESS R334 — IS THIS PATH SEGMENT THE OS ENTROPY SOURCE ITSELF (as a VALUE, not a verb)?
6101///
6102/// **THE DEFECT THIS EXISTS FOR.** Every entropy rule in the `rand` block keys on the CALLEE — a verb
6103/// (`from_os_rng`, `next_u32`, `fill_bytes`) or the source type in RECEIVER position
6104/// (`contains("OsRng")`/`contains("SysRng")`, R320). The idiomatic way to seed a userspace CSPRNG from
6105/// the OS puts the source in ARGUMENT position, where neither key can see it. Executed, one fixture,
6106/// four rows:
6107///
6108/// ```text
6109/// StdRng::try_from_rng(&mut SysRng) ABSENT quinn-proto-0.11.17 endpoint.rs:81
6110/// StdRng::from_rng(&mut OsRng) ABSENT the same hole in the OLD spelling
6111/// StdRng::from_seed([0u8; 32]) ABSENT correct — deterministic, and the control
6112/// StdRng::from_os_rng() ['Rand'] the verb form
6113/// ```
6114///
6115/// Both spellings, so this is NOT a rename residual and predates R320 — whose "stale `OsRng` clause"
6116/// framing would have sent the audit straight past it.
6117///
6118/// **WHY THE ARGUMENT AND NOT THE VERB.** `from_rng`/`try_from_rng` cannot simply be charged: their
6119/// effect is decided by what is passed to them, `from_rng(&mut pcg)` is deterministic, and there are
6120/// 802 `from_seed`/`seed_from_u64` call sites in this machine's registry cache — the deterministic
6121/// population is the larger one. Charging the verb is R330's mistake one crate over. The discriminator
6122/// is the ARGUMENT, so that is what is read.
6123///
6124/// **THE OVER-APPROXIMATION IS DELIBERATE AND STATED.** Naming one of these types as a VALUE is charged
6125/// wherever it appears in an argument list, including moves that do not themselves draw (`Some(OsRng)`,
6126/// `Box::new(SysRng)`). That is sound and it is also the ONLY place candor can see it: a source handed
6127/// off into a struct field is drawn from in some later function whose own call list never mentions it.
6128/// A user type coincidentally named `OsRng` would be charged too — an over-report, the safe direction,
6129/// and cheaper than the silence it replaces.
6130pub fn is_entropy_source_ident(seg: &str) -> bool {
6131 matches!(seg, "OsRng" | "SysRng")
6132}
6133
6134/// SOUNDNESS R1612 — THE EFFECTS A CALL PERFORMS BESIDE THE ONE `classify` RETURNS.
6135///
6136/// `classify` answers ONE effect per path, and that one is the call's PRIMARY: the effect whose locator (path, host,
6137/// program, SQL) is the call's own argument or receiver, which is why every surface rule (`str_arg` capture, the masking
6138/// guard, `fs_kind`) is keyed on it. Some library entry points do a second thing whose locator the caller never sees —
6139/// `next_backoff` draws entropy AND reads the clock, `try_associate` reads `PATH` AND stats the directories on it. Those
6140/// are declared HERE, one row per rule, and charged beside the primary by [`also_effects`], never instead of it.
6141///
6142/// **Why a table and not a set-valued `classify`.** The set is not uniform: the primary carries the call's locator and the
6143/// rest do not. A flat set would invite a charging site to publish the call's literal as the locator of every effect in it
6144/// (a fabricated surface), and the one thing every site must do for a secondary is the OPPOSITE — a secondary `Fs`/`Net`/
6145/// `Exec`/`Db` has a locator nobody can see, so it is marked `incomplete` ([`also_hides_locator`]) and a benign sibling
6146/// literal cannot certify it. And the second-effect hook this replaces (`reads_trust_store`, R338) was asked by ONE of the
6147/// four sites that charge a `classify` answer: `candor-scan`'s main path. The `use`-alias arm, the R1607 trait-dispatch
6148/// arm and the deep engine (`src/lib.rs`) never asked it, so the trust-store `Fs` was charged or silent by spelling and by
6149/// engine. One table, one function, every site — the shape `masks_locator` already has for the same reason (R806/R808).
6150///
6151/// **The bar for a row** is the family's for any charge: an EXECUTED observation of the second effect inside the call, a
6152/// pure neighbour of the same type that is not charged, and `deny <E2>` flipping 0→1 on a consumer. A row whose second
6153/// effect is platform- or configuration-conditional says so in `evidence`; it is charged on the configuration where it was
6154/// observed (the R338 trade: the default build on the platform the crate's users run), and the condition is the row's
6155/// stated over-charge. Do NOT seed this table from `covered.tsv`'s `self_scan_effects`: that oracle's extra effects are a
6156/// union over every branch (1,306 of 2,020 covered rows carry one, 792 of them `Clock` and 468 `Log`, mostly timeouts and
6157/// debug events inside a request), and adopting them is a priced decision, not a transcription (R1613).
6158pub struct AlsoEffect {
6159 /// Crate names (post `-`→`_`) this row applies to.
6160 pub krate: &'static [&'static str],
6161 /// Which paths in those crates.
6162 pub on: AlsoOn,
6163 /// The effects charged beside the primary. An entry equal to the primary is dropped by [`also_effects`].
6164 pub effects: &'static [&'static str],
6165 /// The §2 `fs` direction of an `Fs` in `effects` — `&["read"]` only where the executed window showed no write
6166 /// (no `O_WRONLY`/`O_RDWR`/`O_CREAT`, only `stat`/`access`/read-only opens). Empty means the direction is unknown and
6167 /// the charging site records the poison marker, never a guess.
6168 pub fs_kind: &'static [&'static str],
6169 /// The SOUNDNESS row that carries the evidence.
6170 pub row: &'static str,
6171 /// What was executed, where, and under which condition.
6172 pub evidence: &'static str,
6173}
6174
6175/// How an [`AlsoEffect`] row names its paths — the two shapes the rule tables already use.
6176pub enum AlsoOn {
6177 /// `(type, methods)`: the path's second-to-last segment is `type` and its last is one of `methods` — the key
6178 /// `b11_r1528` matches on, so a row here names exactly the spellings the primary rule beside it names.
6179 Method(&'static str, &'static [&'static str]),
6180 /// A free function: the path ends in `::<leaf>` for one of these leaves.
6181 Leaf(&'static [&'static str]),
6182 /// These exact paths and no others — for a free function whose name a method of the same crate also ends in
6183 /// (`tokio_postgres::connect` against `TlsConnect::connect`), where a [`AlsoOn::Leaf`] tail would charge both.
6184 Exact(&'static [&'static str]),
6185 /// A free function: every path segment after the crate name starts with a lower-case letter (`fastrand::u32`,
6186 /// `fastrand::global_rng::u32`), so no type's method of the same name (`fastrand::Rng::u32`) matches, and neither does a
6187 /// trait-impl spelling such as `fastrand::&Rng::eq` (a segment that is not an identifier).
6188 FreeFn,
6189}
6190
6191impl AlsoOn {
6192 pub fn matches(&self, path: &str) -> bool {
6193 match self {
6194 AlsoOn::Method(ty, methods) => path.rsplit_once("::").is_some_and(|(head, m)| {
6195 methods.contains(&m) && head.rsplit("::").next() == Some(*ty)
6196 }),
6197 AlsoOn::Leaf(leaves) => leaves.iter().any(|l| path.strip_suffix(l).is_some_and(|h| h.ends_with("::"))),
6198 AlsoOn::Exact(paths) => paths.contains(&path),
6199 AlsoOn::FreeFn => path.split("::").skip(1).all(|seg| seg.starts_with(|c: char| c.is_ascii_lowercase() || c == '_')),
6200 }
6201 }
6202}
6203
6204pub const ALSO_EFFECTS: &[AlsoEffect] = &[
6205 // SOUNDNESS R338 — the system trust store, read by a native-tls connector the crate builds INSIDE its own entry point.
6206 // **THE DEFECT.** candor charges `native_tls::TlsConnector::new` `Fs` because on the openssl backend
6207 // it loads the probed cert file/dir from disk. Several calibrated crates construct that connector
6208 // INSIDE a public entry point, so the consumer's call discloses the transport effect and drops the
6209 // `Fs` — `tungstenite::client_tls` reports `['Net']`, and `deny Fs` passes over a real trust-store
6210 // read. `wrap_stream` does `tls_connector.map_or_else(TlsConnector::new, Ok)`
6211 // (`tungstenite-0.30.0/src/tls.rs:54`), so a caller supplying no connector gets `TlsConnector::new()`.
6212 // (The row the coverage gate nominated — `tungstenite::tls::encryption::native_tls::wrap_stream` — is
6213 // unreachable from outside: `mod tls;` is private at `lib.rs:32`. The reachable form is one level up.)
6214 //
6215 // **WHY THIS IS SO SHORT: the sweep found four more crates and every one was PRICED AND REJECTED,
6216 // which is the deliverable rather than a gap.** Callers of `native_tls::TlsConnector::new` in the
6217 // local registry are redis, tungstenite, ureq, minreq and tokio-websockets.
6218 // - `minreq`, `tokio_websockets` — NOT calibrated, so their calls already disclose rather than
6219 // claim purity. No rule can improve them.
6220 // - `redis` — `ActualConnection::new` (`redis-1.6.0/src/connection.rs:932`) constructs it only for a
6221 // `rediss://` address, and the consumer-visible verb is `Client::get_connection`. Charging that
6222 // `Fs` would fabricate on every PLAIN redis user, which is the majority. Rejected on the trade.
6223 // - `ureq` — `agent.rs:265` takes `crate::default_tls_config()`, and ureq's default features are
6224 // `["tls", "gzip"]`: `native-certs` is OFF, so the default build resolves to
6225 // `rtls::root_certs()`'s `#[cfg(not(feature = "native-certs"))]` arm, which is
6226 // `webpki_roots::TLS_SERVER_ROOTS` — COMPILED IN, no disk read (`ureq-2.12.1/src/rtls.rs:82`).
6227 // Charging it would fabricate on the default build. Rejected on the evidence.
6228 //
6229 // What is left is tungstenite's two explicitly-TLS-named entry points, where no caller is
6230 // over-charged: `client_tls` passes `None` for the connector unconditionally, and
6231 // `client_tls_with_config`'s callers are by definition doing TLS. **`tungstenite::connect` is a stated
6232 // residual, not an oversight** — it serves `ws://` and `wss://` alike, so charging it would hit real
6233 // plain-WebSocket users; the scheme is often a string literal candor already captures in `str_arg`,
6234 // which is the shape a future fix should use rather than a blanket charge.
6235 AlsoEffect {
6236 krate: &["tungstenite"],
6237 on: AlsoOn::Leaf(&["client_tls", "client_tls_with_config"]),
6238 effects: &["Fs"],
6239 fs_kind: &["read"],
6240 row: "R338",
6241 evidence: "openssl backend: TlsConnector::new loads the probed cert file/dir; client_tls passes no connector \
6242 (tungstenite-0.30.0 src/tls.rs:54). macOS (Security framework) reads no file.",
6243 },
6244 // SOUNDNESS R1528 (b11) — elasticsearch builds a reqwest client with the DEFAULT `native-tls` feature.
6245 AlsoEffect {
6246 krate: &["elasticsearch"],
6247 on: AlsoOn::Method("Transport", &["single_node", "static_node_list", "sniffing_node_list", "cloud"]),
6248 effects: &["Fs"],
6249 fs_kind: &["read"],
6250 row: "R1598",
6251 evidence: "EXECUTED under strace in rust:1-bookworm: Transport::single_node opens openssl.cnf, cert.pem, certs/, \
6252 ca-certificates.crt; 0 opens on macOS. Primary Env is the proxy-variable read.",
6253 },
6254 AlsoEffect {
6255 krate: &["elasticsearch"],
6256 on: AlsoOn::Method("TransportBuilder", &["build"]),
6257 effects: &["Fs"],
6258 fs_kind: &["read"],
6259 row: "R1598",
6260 evidence: "as Transport::single_node (EXECUTED under strace, Linux/openssl).",
6261 },
6262 // SOUNDNESS R1612 — R1599's four rule rows.
6263 AlsoEffect {
6264 krate: &["backoff"],
6265 on: AlsoOn::Method("ExponentialBackoff", &["next_backoff"]),
6266 effects: &["Clock"],
6267 fs_kind: &[],
6268 row: "R1612",
6269 evidence: "EXECUTED (macOS, DYLD interposer): next_backoff on a default ExponentialBackoff = 1 getentropy (the \
6270 Rand primary) + 1 clock_gettime (the max_elapsed_time check reads clock.now()). Pure neighbour \
6271 ExponentialBackoffBuilder::new: 0 events.",
6272 },
6273 AlsoEffect {
6274 krate: &["grep_cli"],
6275 on: AlsoOn::Method("DecompressionMatcherBuilder", &["associate", "try_associate"]),
6276 effects: &["Fs"],
6277 fs_kind: &["read"],
6278 row: "R1612",
6279 evidence: "EXECUTED (macOS, DYLD interposer): a relative program (`gzip`) = getenv(PATH) (the Env primary) + \
6280 stat(<PATH dir>/gzip) per directory until found; an absolute program = 0 events, the same accepted \
6281 over-charge as the Env primary. Pure neighbour DecompressionMatcherBuilder::new: 0 events.",
6282 },
6283 AlsoEffect {
6284 krate: &["time"],
6285 on: AlsoOn::Method("UtcOffset", &["current_local_offset"]),
6286 effects: &["Fs"],
6287 fs_kind: &["read"],
6288 row: "R1612",
6289 evidence: "EXECUTED (macOS, DYLD interposer), TZ unset: 1 clock_gettime (the Clock primary) + localtime_r + \
6290 access(/etc/localtime). With TZ set the interposer saw no file call (libc's own zone-file open is \
6291 not interposable), so the TZ-set arm is not claimed either way. libc caches the zone: first call per \
6292 process. Pure neighbour UtcOffset::from_hms: 0 events.",
6293 },
6294 AlsoEffect {
6295 krate: &["portable_pty"],
6296 on: AlsoOn::Method("CommandBuilder", &["new", "from_argv", "new_default_prog"]),
6297 effects: &["Fs"],
6298 fs_kind: &["read"],
6299 row: "R1612",
6300 evidence: "EXECUTED (macOS, DYLD interposer), SHELL unset: each of the three ctors = access(<passwd shell>, X_OK) \
6301 (get_base_env -> get_shell, cmdbuilder.rs:43-71); SHELL set: 0 events. Charged on the unset arm, \
6302 which is the default in containers and service managers; SHELL-set callers are the stated \
6303 over-charge. Pure neighbour CommandBuilder::arg: 0 events.",
6304 },
6305
6306 // SOUNDNESS R1617 — LOG ROWS: a call into a logging/tracing framework INSIDE a dependency is `Log` at the consumer's call
6307 // (SPEC.md:427-431; FABLE-LOG-POLICY, R1613), whether or not a subscriber is installed. Every row below was EXECUTED with a
6308 // `log::Log` implementation AND a `tracing::Subscriber` installed (macOS, tokio-postgres 0.7.18, postgres 0.19.14,
6309 // deadpool-postgres 0.14.2, reqwest 0.13.5, default features, no `log` `max_level_*` feature), the record's file:line printed
6310 // inside the call window. CONDITION, for every `tokio_postgres` row: the `debug!` sites are `query.rs:45/:220`
6311 // (gated by `log_enabled!(Level::Debug)`), `prepare.rs:122/:124`, `copy_out.rs:14`, `copy_in.rs:195`, `simple_query.rs:35/:47`:
6312 // they emit at DEBUG, so a logger enabled at INFO saw 0 records (EXECUTED) and the row is charged on the DEBUG-enabled
6313 // configuration the policy prices. NOT charged, EXECUTED 0 records at DEBUG: the `query_typed*` / `execute_typed` family (their
6314 // own protocol path has no log site) and `Client::is_closed` / `cancel_token` / `build_transaction` (no I/O).
6315 AlsoEffect {
6316 krate: &["tokio_postgres", "postgres"],
6317 on: AlsoOn::Method(
6318 "Client",
6319 &["query", "query_one", "query_opt", "query_raw", "execute", "batch_execute", "simple_query", "prepare",
6320 "prepare_typed", "copy_in", "copy_out", "transaction"],
6321 ),
6322 effects: &["Log"],
6323 fs_kind: &[],
6324 row: "R1617",
6325 evidence: "EXECUTED, logger at DEBUG: 1 `log` record from tokio_postgres per call (query* query.rs:45, execute query.rs:220, \
6326 batch_execute/transaction simple_query.rs:47, simple_query :35, prepare* prepare.rs:122, copy_out copy_out.rs:14, \
6327 copy_in copy_in.rs:195); a `&str` statement adds the prepare.rs:122 record. 0 at INFO. Pure neighbours \
6328 is_closed/cancel_token/build_transaction: 0. Same records via postgres::Client (it wraps tokio_postgres).",
6329 },
6330 AlsoEffect {
6331 krate: &["tokio_postgres"],
6332 on: AlsoOn::Method("Client", &["execute_raw", "simple_query_raw"]),
6333 effects: &["Log"],
6334 fs_kind: &[],
6335 row: "R1617",
6336 evidence: "EXECUTED, logger at DEBUG: execute_raw query.rs:220, simple_query_raw simple_query.rs:35, 1 record each. 0 at INFO. \
6337 Not charged (0 records, EXECUTED): query_typed_raw, query_typed_one, execute_typed.",
6338 },
6339 AlsoEffect {
6340 krate: &["postgres"],
6341 on: AlsoOn::Method("Client", &["is_valid"]),
6342 effects: &["Log"],
6343 fs_kind: &[],
6344 row: "R1617",
6345 evidence: "EXECUTED, logger at DEBUG: is_valid sends simple_query(\"\") -> simple_query.rs:35, 1 record.",
6346 },
6347 AlsoEffect {
6348 krate: &["tokio_postgres", "postgres"],
6349 on: AlsoOn::Method(
6350 "Transaction",
6351 &["query", "query_one", "query_opt", "query_raw", "execute", "batch_execute", "simple_query", "prepare",
6352 "prepare_typed", "copy_in", "copy_out", "transaction", "commit", "rollback"],
6353 ),
6354 effects: &["Log"],
6355 fs_kind: &[],
6356 row: "R1617",
6357 evidence: "EXECUTED, logger at DEBUG, inside an open transaction: the same record per verb as Client (query* query.rs:45, \
6358 execute :220, prepare* prepare.rs:122, copy_* copy_out.rs:14/copy_in.rs:195, simple_query :35); \
6359 batch_execute/transaction/commit/rollback each 1 simple_query.rs:47 record. 0 at INFO. Not charged: query_typed (0).",
6360 },
6361 AlsoEffect {
6362 krate: &["deadpool_postgres"],
6363 on: AlsoOn::Method(
6364 "Client",
6365 &["query", "query_one", "query_opt", "query_raw", "execute", "batch_execute", "prepare", "prepare_typed",
6366 "prepare_cached", "prepare_typed_cached", "transaction"],
6367 ),
6368 effects: &["Log"],
6369 fs_kind: &[],
6370 row: "R1617",
6371 evidence: "EXECUTED, logger at DEBUG, through deadpool's GenericClient spelling and its inherent prepare_cached/\
6372 prepare_typed_cached: tokio_postgres records as above. prepare_cached/prepare_typed_cached log only on a \
6373 statement-cache MISS (prepare.rs:122); a warm cache hit: 0 records (EXECUTED), the same cold-cache arm the \
6374 Db primary already charges unconditionally.",
6375 },
6376 AlsoEffect {
6377 krate: &["deadpool_postgres"],
6378 on: AlsoOn::Method("ClientWrapper", &["prepare_cached", "prepare_typed_cached", "transaction"]),
6379 effects: &["Log"],
6380 fs_kind: &[],
6381 row: "R1617",
6382 evidence: "EXECUTED, logger at DEBUG: the inherent ClientWrapper methods; prepare_* only on a statement-cache miss (warm: 0).",
6383 },
6384 AlsoEffect {
6385 krate: &["deadpool_postgres"],
6386 on: AlsoOn::Method(
6387 "Transaction",
6388 &["query", "query_one", "query_opt", "query_raw", "execute", "batch_execute", "prepare", "prepare_typed",
6389 "prepare_cached", "prepare_typed_cached", "transaction", "commit", "rollback"],
6390 ),
6391 effects: &["Log"],
6392 fs_kind: &[],
6393 row: "R1617",
6394 evidence: "EXECUTED, logger at DEBUG, deadpool Transaction through GenericClient: tokio_postgres records as Client; \
6395 commit/rollback/transaction 1 simple_query.rs:47 each; prepare_*_cached only on a cache miss.",
6396 },
6397 // reqwest 0.13.5. FABLE-LOG-POLICY read the log as proxy-path only (`connect.rs:784/:805`); EXECUTED, that is wrong: the call logs
6398 // WITHOUT a proxy. Every `execute` emits `tracing` events from hyper-util's pool/client (4 on a pooled keep-alive reuse:
6399 // pool.rs:659/:267/:352/:400), a fresh connection adds the `log` record `connect.rs:929` ("starting new connection") and 6 more
6400 // hyper-util events (11 in all); `connect.rs:784` ("proxy(..) intercepts") appears only when HTTP_PROXY/HTTPS_PROXY selects a
6401 // proxy (12 records with it). Measured with BOTH a `log` logger and a `tracing` subscriber at TRACE; the `tracing` events need
6402 // a subscriber, the `log` record a logger. Builders `get`/`post`/`build` and `RequestBuilder` setters: 0 records.
6403 AlsoEffect {
6404 krate: &["reqwest"],
6405 on: AlsoOn::Method("Client", &["execute"]),
6406 effects: &["Log"],
6407 fs_kind: &[],
6408 row: "R1617",
6409 evidence: "EXECUTED (async Client + blocking::Client), logger + tracing subscriber installed: pooled reuse 4 tracing events \
6410 (hyper_util pool.rs); new connection 11 (connect.rs:929 `log` + hyper_util); HTTP_PROXY adds connect.rs:784. \
6411 Client::get/build: 0.",
6412 },
6413 AlsoEffect {
6414 krate: &["reqwest"],
6415 on: AlsoOn::Method("RequestBuilder", &["send"]),
6416 effects: &["Log"],
6417 fs_kind: &[],
6418 row: "R1617",
6419 evidence: "EXECUTED (async + blocking), as Client::execute: 11 records on a new connection. RequestBuilder::get/build: 0.",
6420 },
6421 // SOUNDNESS R1617 — NON-LOG EXTRAS. postgres / tokio_postgres connect: EXECUTED against postgres:16 (macOS, DYLD interposer).
6422 // Net: connect(127.0.0.1:55432). Ipc: connect(AF_UNIX path) for a unix-socket host. Rand: 1 getentropy for the SCRAM nonce,
6423 // ONLY when the server asks SCRAM (a `trust` server: 0 getentropy, same runtime). A single call performs ONE of Net/Ipc
6424 // (by the host string) and Rand only against SCRAM, so each is a configuration arm charged where observed; the Db primary
6425 // already charges the call without a host. deadpool-postgres's consumer-callable `Pool::{get, timeout_get}` is a primary `Db`
6426 // since R1618 (it was an unclassified path, so the call read pure), and carries the same three extras below.
6427 AlsoEffect {
6428 krate: &["tokio_postgres", "postgres"],
6429 on: AlsoOn::Method("Config", &["connect"]),
6430 effects: &["Net", "Ipc", "Rand"],
6431 fs_kind: &[],
6432 row: "R1617",
6433 evidence: "EXECUTED (postgres:16, interposer): TCP host = connect(127.0.0.1:port) [Net]; unix host = connect(unix:<dir>/.s.PGSQL.n) \
6434 [Ipc]; SCRAM server = 1 getentropy [Rand], trust server = 0. Pure neighbour Config::from_str / Config::host: 0 events. \
6435 connect_raw (caller's stream) is not claimed.",
6436 },
6437 AlsoEffect {
6438 krate: &["tokio_postgres"],
6439 on: AlsoOn::Exact(&["tokio_postgres::connect"]),
6440 effects: &["Net", "Ipc", "Rand"],
6441 fs_kind: &[],
6442 row: "R1617",
6443 evidence: "EXECUTED as Config::connect (tokio_postgres::connect parses the string and calls it): TCP 127.0.0.1 Net, unix \
6444 socket Ipc, SCRAM nonce 1 getentropy Rand (trust server: 0).",
6445 },
6446 AlsoEffect {
6447 krate: &["postgres"],
6448 on: AlsoOn::Method("Client", &["connect"]),
6449 effects: &["Net", "Ipc", "Rand"],
6450 fs_kind: &[],
6451 row: "R1617",
6452 evidence: "EXECUTED as Config::connect (postgres::Client::connect parses and calls it): TCP Net, SCRAM Rand. (The unix-host arm \
6453 was EXECUTED through postgres::Config::connect.)",
6454 },
6455 // SOUNDNESS R1618 — deadpool_postgres `Pool::{get, timeout_get}` (primary Db, see `classify`). EXECUTED (postgres:16, macOS,
6456 // DYLD interposer, rows in handoff `rust-r1617/`): a COLD pool's get = connect(127.0.0.1:55432) [Net] + 1 getentropy [Rand] (the
6457 // SCRAM nonce; 0 against a trust server, as for `Config::connect`); a WARM pool's get = 0 connects, 0 entropy. The unix-host arm
6458 // is the same `Config::connect` the manager calls. OVER-CHARGE, stated: a call that finds an idle object (the steady state of a
6459 // long-lived pool) performs none of these; the cold arm is the only one a static scan cannot rule out. `Log`: 0 records.
6460 AlsoEffect {
6461 krate: &["deadpool_postgres"],
6462 on: AlsoOn::Method("Pool", &["get", "timeout_get"]),
6463 effects: &["Net", "Ipc", "Rand"],
6464 fs_kind: &[],
6465 row: "R1618",
6466 evidence: "EXECUTED cold pool: connect(127.0.0.1:55432) + 1 getentropy; warm pool (idle object): 0 of both. get and timeout_get \
6467 identical. Pure neighbours status/is_closed/manager/resize/close: 0 events.",
6468 },
6469 // SOUNDNESS R1618 — `build_transaction().start()` = BEGIN, and one `log` record like `transaction()` (simple_query.rs:47). EXECUTED
6470 // through the tokio_postgres, postgres (sync; 2 records: BEGIN and the drop's ROLLBACK) and deadpool spellings. The consumer path of
6471 // a chained call is the receiver's type (`Client::start`, `Object::start`); a held builder is `TransactionBuilder::start`.
6472 AlsoEffect {
6473 krate: &["tokio_postgres", "postgres", "deadpool_postgres"],
6474 on: AlsoOn::Method("Client", &["start"]),
6475 effects: &["Log"],
6476 fs_kind: &[],
6477 row: "R1618",
6478 evidence: "EXECUTED, logger at DEBUG: build_transaction().start() = 1 record, simple_query.rs:47 (also with isolation_level/read_only \
6479 in the chain). 0 at INFO. build_transaction / isolation_level / read_only alone: 0.",
6480 },
6481 AlsoEffect {
6482 krate: &["tokio_postgres", "postgres", "deadpool_postgres"],
6483 on: AlsoOn::Method("TransactionBuilder", &["start"]),
6484 effects: &["Log"],
6485 fs_kind: &[],
6486 row: "R1618",
6487 evidence: "EXECUTED, logger at DEBUG: a held TransactionBuilder's start = 1 record, simple_query.rs:47 (sync postgres: 2, BEGIN and the \
6488 drop's ROLLBACK).",
6489 },
6490 AlsoEffect {
6491 krate: &["deadpool_postgres"],
6492 on: AlsoOn::Method("Object", &["start"]),
6493 effects: &["Log"],
6494 fs_kind: &[],
6495 row: "R1618",
6496 evidence: "EXECUTED, logger at DEBUG: pooled Object .build_transaction().start() = 1 record, simple_query.rs:47.",
6497 },
6498 // SOUNDNESS R1618 — sqlx MySQL connect: the facade's primary is Db (`VERBS` for `::connect`, `sqlx_r1528` for `::connect_with`),
6499 // and every connecting spelling EXECUTED against mysql:8 (macOS, sqlx 0.9.0, tls-rustls, DYLD interposer) = connect(127.0.0.1:53306)
6500 // [Net] + 5 getentropy [Rand] on the default `ssl-mode=preferred` (also `required`; `ssl-mode=disabled`: connect only, 0 entropy);
6501 // `?socket=<path>` = connect(AF_UNIX path) [Ipc] with the same entropy. Spellings executed: MySqlConnection::{connect,
6502 // connect_with}, MySqlPool::{connect, connect_with}, MySqlPoolOptions::{connect, connect_with}, MySqlConnectOptions::connect
6503 // (the `ConnectOptions` trait method). `connect_lazy` opens nothing (R1589). OVER-CHARGE, stated: a `disabled` URL draws no
6504 // entropy, and Net / Ipc are the two arms of one call (the host string picks). `ssl-ca=<file>` opens that file (Fs, EXECUTED) and
6505 // is NOT charged here: see R1618, a URL-conditional Fs needs a literal-keyed charge, not a table row. Postgres and sqlite via
6506 // sqlx are not executed and not claimed.
6507 AlsoEffect {
6508 krate: &["sqlx"],
6509 on: AlsoOn::Method("MySqlConnection", &["connect", "connect_with"]),
6510 effects: &["Net", "Ipc", "Rand"],
6511 fs_kind: &[],
6512 row: "R1618",
6513 evidence: "EXECUTED (mysql:8, interposer): connect(127.0.0.1) + 5 getentropy on the default/required TLS arms; ssl-mode=disabled: connect \
6514 only; ?socket= : connect(AF_UNIX). Neighbour connect_lazy and MySqlConnectOptions parse/new: 0 events.",
6515 },
6516 AlsoEffect {
6517 krate: &["sqlx"],
6518 on: AlsoOn::Method("MySqlPool", &["connect", "connect_with"]),
6519 effects: &["Net", "Ipc", "Rand"],
6520 fs_kind: &[],
6521 row: "R1618",
6522 evidence: "EXECUTED (mysql:8): Pool::connect / connect_with open one connection eagerly = connect + 5 getentropy; connect_lazy: 0.",
6523 },
6524 AlsoEffect {
6525 krate: &["sqlx"],
6526 on: AlsoOn::Method("MySqlPoolOptions", &["connect", "connect_with"]),
6527 effects: &["Net", "Ipc", "Rand"],
6528 fs_kind: &[],
6529 row: "R1618",
6530 evidence: "EXECUTED (mysql:8): PoolOptions::connect / connect_with = connect + 5 getentropy. max_connections / new: 0.",
6531 },
6532 AlsoEffect {
6533 krate: &["sqlx"],
6534 on: AlsoOn::Method("MySqlConnectOptions", &["connect"]),
6535 effects: &["Net", "Ipc", "Rand"],
6536 fs_kind: &[],
6537 row: "R1618",
6538 evidence: "EXECUTED (mysql:8): ConnectOptions::connect on MySqlConnectOptions = connect + 5 getentropy.",
6539 },
6540 // fastrand 2.5.0 seeds its THREAD-LOCAL generator from `Instant::now()` on a thread's first use (global_rng.rs:203-212), so
6541 // the first fastrand call per thread reads the clock. EXECUTED (macOS, fresh thread, interposer): fastrand::{u32, seed,
6542 // get_seed, alphabetic}, Rng::new, Rng::default = 1 clock read on the first call, 0 on the second; Rng::with_seed and an
6543 // instance draw on a seeded Rng = 0. tempfile (every name-generating entry) and async-executor (Executor::run) inherit it.
6544 AlsoEffect {
6545 krate: &["fastrand"],
6546 on: AlsoOn::FreeFn,
6547 effects: &["Clock"],
6548 fs_kind: &[],
6549 row: "R1617",
6550 evidence: "EXECUTED (fresh thread, interposer): fastrand::u32/seed/get_seed/alphabetic = 1 clock read on the thread's FIRST use \
6551 (Instant::now() seeds the thread-local RNG), 0 on the second call.",
6552 },
6553 AlsoEffect {
6554 krate: &["fastrand"],
6555 on: AlsoOn::Method("Rng", &["new", "default"]),
6556 effects: &["Clock"],
6557 fs_kind: &[],
6558 row: "R1617",
6559 evidence: "EXECUTED (fresh thread): Rng::new and Rng::default fork the thread-local generator, whose first use reads the clock: \
6560 1 on the first call per thread, 0 after. Rng::with_seed: 0.",
6561 },
6562 AlsoEffect {
6563 krate: &["tempfile"],
6564 on: AlsoOn::Method(
6565 "Builder",
6566 &["tempfile", "tempfile_in", "make", "make_in", "tempdir", "tempdir_in"],
6567 ),
6568 effects: &["Clock"],
6569 fs_kind: &[],
6570 row: "R1617",
6571 evidence: "EXECUTED (fresh thread, interposer): 1 clock read on the first name generated per thread (create_helper -> \
6572 fastrand::Rng::new), 0 on the second call. Builder::new().prefix().suffix().rand_bytes(): 0.",
6573 },
6574 AlsoEffect {
6575 krate: &["tempfile"],
6576 on: AlsoOn::Method(
6577 "NamedTempFile",
6578 &["new", "new_in", "with_prefix", "with_prefix_in", "with_suffix", "with_suffix_in"],
6579 ),
6580 effects: &["Clock"],
6581 fs_kind: &[],
6582 row: "R1617",
6583 evidence: "EXECUTED (fresh thread): 1 clock read on the first name generated per thread, 0 on the second. NamedTempFile::close \
6584 and ::persist (no name generation): 0.",
6585 },
6586 AlsoEffect {
6587 krate: &["tempfile"],
6588 on: AlsoOn::Method(
6589 "TempDir",
6590 &["new", "new_in", "with_prefix", "with_prefix_in", "with_suffix", "with_suffix_in"],
6591 ),
6592 effects: &["Clock"],
6593 fs_kind: &[],
6594 row: "R1617",
6595 evidence: "EXECUTED (fresh thread): 1 clock read on the first name generated per thread, 0 on the second.",
6596 },
6597 AlsoEffect {
6598 krate: &["tempfile"],
6599 on: AlsoOn::Leaf(&["tempfile", "tempfile_in", "tempdir", "tempdir_in"]),
6600 effects: &["Clock"],
6601 fs_kind: &[],
6602 row: "R1617",
6603 evidence: "EXECUTED (fresh thread): the free functions tempfile()/tempfile_in()/tempdir()/tempdir_in(), 1 clock read on the \
6604 first name generated per thread.",
6605 },
6606
6607 // SOUNDNESS R1623 — sqlx 0.9.0 POSTGRES, MYSQL-ACQUIRE and SQLITE second effects (R1618's residual (c)). EXECUTED on macOS (DYLD
6608 // interposer) against postgres:16 with TLS on and SCRAM, mysql:8 and real sqlite files; handoff `rust-r1623/`.
6609 // Postgres connect (any of the 8 spellings, with options already built): 1 connect (TCP, or AF_UNIX for a socket host) +
6610 // 1-6 `getentropy` (1 with `sslmode=disable`: the SCRAM nonce only; 5-6 with TLS). A URL SPELLING (`connect(url)`,
6611 // `connect_lazy(url)`, `.parse()`) also parses the URL, and the parse reads the twelve `PG*` variables and `stat`s the three
6612 // candidate unix-socket directories, plus opens `~/.pgpass` when the URL has no password (14 `getenv`, 1 `open`).
6613 // Acquire-class (`acquire`, `begin`, `begin_with`) on a COLD pool is the same connect; on a WARM pool (an idle connection)
6614 // it performs NONE of these (EXECUTED: 0 connect, 0 entropy, 0 stat). That is the stated over-charge, as for deadpool's `Pool::get`.
6615 // SQLite connect: opens the database file read-write (`open(db, +creat)` under `mode=rwc`, plus `stat`/`lstat` of its path),
6616 // opens `/dev/urandom` (SQLite seeds its PRNG on first use in the process: Rand), and reads `SQLITE_TMPDIR`/`TMPDIR`/`RUST_MIN_STACK`
6617 // (the last is std's thread-spawn read). `sqlite::memory:` opens only `/dev/urandom` and reads the three variables: the file
6618 // open is the URL-conditional over-charge. Not charged: `Clock` (the SCRAM key derivation yields to the runtime between
6619 // iterations, so tokio reads the clock ~2,500 times; it is scheduler plumbing, uncharged as for R1618), sslrootcert/ssl-ca `Fs`
6620 // (URL-conditional, R1618 residual (b)), and the per-query `stat` of `-journal`/`-wal` on SQLite (`Query::execute` is driver-agnostic).
6621 AlsoEffect {
6622 krate: &["sqlx"],
6623 on: AlsoOn::Method("PgConnection", &["connect", "connect_with"]),
6624 effects: &["Net", "Ipc", "Rand"],
6625 fs_kind: &[],
6626 row: "R1623",
6627 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6628 },
6629 AlsoEffect {
6630 krate: &["sqlx"],
6631 on: AlsoOn::Method("PgPool", &["connect", "connect_with"]),
6632 effects: &["Net", "Ipc", "Rand"],
6633 fs_kind: &[],
6634 row: "R1623",
6635 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6636 },
6637 AlsoEffect {
6638 krate: &["sqlx"],
6639 on: AlsoOn::Method("PgPoolOptions", &["connect", "connect_with"]),
6640 effects: &["Net", "Ipc", "Rand"],
6641 fs_kind: &[],
6642 row: "R1623",
6643 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6644 },
6645 AlsoEffect {
6646 krate: &["sqlx"],
6647 on: AlsoOn::Method("PgListener", &["connect", "connect_with"]),
6648 effects: &["Net", "Ipc", "Rand"],
6649 fs_kind: &[],
6650 row: "R1623",
6651 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6652 },
6653 AlsoEffect {
6654 krate: &["sqlx"],
6655 on: AlsoOn::Method("PgConnectOptions", &["connect"]),
6656 effects: &["Net", "Ipc", "Rand"],
6657 fs_kind: &[],
6658 row: "R1623",
6659 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6660 },
6661 AlsoEffect {
6662 krate: &["sqlx"],
6663 on: AlsoOn::Method("PgPool", &["acquire", "begin", "begin_with"]),
6664 effects: &["Net", "Ipc", "Rand"],
6665 fs_kind: &[],
6666 row: "R1623",
6667 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6668 },
6669 // The `*PoolOptions` acquire rows below name a type that has no `acquire`: `let p = PgPoolOptions::new().connect_lazy(u)
6670 // .unwrap(); p.acquire()` is typed by the builder walk as its ROOT (`PgPoolOptions`), not the `PgPool` it returns, so the
6671 // call forms `PgPoolOptions::acquire` (R1608 family: the chain root is the guess). Without them that spelling reads `Db` only.
6672 AlsoEffect {
6673 krate: &["sqlx"],
6674 on: AlsoOn::Method("PgPoolOptions", &["acquire", "begin", "begin_with"]),
6675 effects: &["Net", "Ipc", "Rand"],
6676 fs_kind: &[],
6677 row: "R1623",
6678 evidence: "EXECUTED (postgres:16, TLS+SCRAM, interposer): 1 connect (AF_UNIX for a socket host) + 1-6 getentropy per call with options already built; a warm pool's acquire/begin: 0 of both.",
6679 },
6680 AlsoEffect {
6681 krate: &["sqlx"],
6682 on: AlsoOn::Method("MySqlPool", &["acquire", "begin", "begin_with"]),
6683 effects: &["Net", "Ipc", "Rand"],
6684 fs_kind: &[],
6685 row: "R1623",
6686 evidence: "EXECUTED (mysql:8, interposer): a cold pool's acquire/begin = 1 connect + 5 getentropy; a warm pool's acquire: 0 of both.",
6687 },
6688 AlsoEffect {
6689 krate: &["sqlx"],
6690 on: AlsoOn::Method("MySqlPoolOptions", &["acquire", "begin", "begin_with"]),
6691 effects: &["Net", "Ipc", "Rand"],
6692 fs_kind: &[],
6693 row: "R1623",
6694 evidence: "EXECUTED (mysql:8, interposer): a cold pool's acquire/begin = 1 connect + 5 getentropy; a warm pool's acquire: 0 of both.",
6695 },
6696 AlsoEffect {
6697 krate: &["sqlx"],
6698 on: AlsoOn::Method("PgConnection", &["connect"]),
6699 effects: &["Env", "Fs"],
6700 fs_kind: &["read"],
6701 row: "R1623",
6702 evidence: "EXECUTED (postgres:16, interposer): a URL spelling parses the URL = 12 PG* getenv + 3 stat of the unix-socket candidate dirs (+ HOME/PGPASSFILE and an open of ~/.pgpass when the URL has no password). connect_with(&opts) parses nothing.",
6703 },
6704 AlsoEffect {
6705 krate: &["sqlx"],
6706 on: AlsoOn::Method("PgPool", &["connect"]),
6707 effects: &["Env", "Fs"],
6708 fs_kind: &["read"],
6709 row: "R1623",
6710 evidence: "EXECUTED (postgres:16, interposer): a URL spelling parses the URL = 12 PG* getenv + 3 stat of the unix-socket candidate dirs (+ HOME/PGPASSFILE and an open of ~/.pgpass when the URL has no password). connect_with(&opts) parses nothing.",
6711 },
6712 AlsoEffect {
6713 krate: &["sqlx"],
6714 on: AlsoOn::Method("PgPoolOptions", &["connect"]),
6715 effects: &["Env", "Fs"],
6716 fs_kind: &["read"],
6717 row: "R1623",
6718 evidence: "EXECUTED (postgres:16, interposer): a URL spelling parses the URL = 12 PG* getenv + 3 stat of the unix-socket candidate dirs (+ HOME/PGPASSFILE and an open of ~/.pgpass when the URL has no password). connect_with(&opts) parses nothing.",
6719 },
6720 AlsoEffect {
6721 krate: &["sqlx"],
6722 on: AlsoOn::Method("PgListener", &["connect"]),
6723 effects: &["Env", "Fs"],
6724 fs_kind: &["read"],
6725 row: "R1623",
6726 evidence: "EXECUTED (postgres:16, interposer): a URL spelling parses the URL = 12 PG* getenv + 3 stat of the unix-socket candidate dirs (+ HOME/PGPASSFILE and an open of ~/.pgpass when the URL has no password). connect_with(&opts) parses nothing.",
6727 },
6728 AlsoEffect {
6729 krate: &["sqlx"],
6730 on: AlsoOn::Method("PgPool", &["connect_lazy"]),
6731 effects: &["Fs"],
6732 fs_kind: &["read"],
6733 row: "R1623",
6734 evidence: "EXECUTED (postgres:16, interposer): connect_lazy(url) = 12 PG* getenv [Env primary] + 3 stat, 0 connections.",
6735 },
6736 AlsoEffect {
6737 krate: &["sqlx"],
6738 on: AlsoOn::Method("PgPoolOptions", &["connect_lazy"]),
6739 effects: &["Fs"],
6740 fs_kind: &["read"],
6741 row: "R1623",
6742 evidence: "EXECUTED (postgres:16, interposer): connect_lazy(url) = 12 PG* getenv [Env primary] + 3 stat, 0 connections.",
6743 },
6744 AlsoEffect {
6745 krate: &["tracing_subscriber"],
6746 on: AlsoOn::Method("OffsetTime", &["local_rfc_3339"]),
6747 effects: &["Fs"],
6748 fs_kind: &["read"],
6749 row: "R1528",
6750 evidence: "EXECUTED (tracing-subscriber 0.3.23, macOS interposer, TZ unset): 1 clock_gettime [Clock primary] + access(/etc/localtime) \
6751 (the `time` crate's current_local_offset, R1612); a pure control: 0.",
6752 },
6753 AlsoEffect {
6754 krate: &["sea_orm"],
6755 on: AlsoOn::Method("SqlxPostgresConnector", &["accepts"]),
6756 effects: &["Fs"],
6757 fs_kind: &["read"],
6758 row: "R1528",
6759 evidence: "EXECUTED (sea-orm 2.0.4, macOS interposer): accepts(\"postgres://u@h/db\") = 13 PG* getenv [Env primary] + stat of \
6760 /var/run/postgresql, /private/tmp, /tmp `.s.PGSQL.5432` + open ~/.pgpass; a `mysql://` string and a pure control: 0.",
6761 },
6762 AlsoEffect {
6763 krate: &["sqlx"],
6764 on: AlsoOn::Method("PgConnectOptions", &["new", "new_without_pgpass", "default", "from_str", "from_url"]),
6765 effects: &["Fs"],
6766 fs_kind: &["read"],
6767 row: "R1623",
6768 evidence: "EXECUTED (postgres:16, interposer): 12 PG* getenv [Env primary] + 3 stat of the unix-socket candidate dirs; new()/default() also open ~/.pgpass (14 getenv) when no password is set.",
6769 },
6770 AlsoEffect {
6771 krate: &["sqlx"],
6772 on: AlsoOn::Method("SqliteConnection", &["connect", "connect_with"]),
6773 effects: &["Env", "Fs", "Rand"],
6774 fs_kind: &[],
6775 row: "R1623",
6776 evidence: "EXECUTED (sqlite via sqlx 0.9.0, interposer): connect opens the db file read-write (+creat under mode=rwc) with stat/lstat of its path, opens /dev/urandom (first sqlite use in the process), reads SQLITE_TMPDIR/TMPDIR/RUST_MIN_STACK. sqlite::memory: opens only /dev/urandom. Fs direction depends on mode=: unknown.",
6777 },
6778 AlsoEffect {
6779 krate: &["sqlx"],
6780 on: AlsoOn::Method("SqlitePool", &["connect", "connect_with"]),
6781 effects: &["Env", "Fs", "Rand"],
6782 fs_kind: &[],
6783 row: "R1623",
6784 evidence: "EXECUTED (sqlite via sqlx 0.9.0, interposer): connect opens the db file read-write (+creat under mode=rwc) with stat/lstat of its path, opens /dev/urandom (first sqlite use in the process), reads SQLITE_TMPDIR/TMPDIR/RUST_MIN_STACK. sqlite::memory: opens only /dev/urandom. Fs direction depends on mode=: unknown.",
6785 },
6786 AlsoEffect {
6787 krate: &["sqlx"],
6788 on: AlsoOn::Method("SqlitePoolOptions", &["connect", "connect_with"]),
6789 effects: &["Env", "Fs", "Rand"],
6790 fs_kind: &[],
6791 row: "R1623",
6792 evidence: "EXECUTED (sqlite via sqlx 0.9.0, interposer): connect opens the db file read-write (+creat under mode=rwc) with stat/lstat of its path, opens /dev/urandom (first sqlite use in the process), reads SQLITE_TMPDIR/TMPDIR/RUST_MIN_STACK. sqlite::memory: opens only /dev/urandom. Fs direction depends on mode=: unknown.",
6793 },
6794 AlsoEffect {
6795 krate: &["sqlx"],
6796 on: AlsoOn::Method("SqliteConnectOptions", &["connect", "connect_with"]),
6797 effects: &["Env", "Fs", "Rand"],
6798 fs_kind: &[],
6799 row: "R1623",
6800 evidence: "EXECUTED (sqlite via sqlx 0.9.0, interposer): connect opens the db file read-write (+creat under mode=rwc) with stat/lstat of its path, opens /dev/urandom (first sqlite use in the process), reads SQLITE_TMPDIR/TMPDIR/RUST_MIN_STACK. sqlite::memory: opens only /dev/urandom. Fs direction depends on mode=: unknown.",
6801 },
6802 AlsoEffect {
6803 krate: &["sqlx"],
6804 on: AlsoOn::Method("SqlitePool", &["acquire", "begin", "begin_with"]),
6805 effects: &["Env", "Fs", "Rand"],
6806 fs_kind: &[],
6807 row: "R1623",
6808 evidence: "EXECUTED (sqlite, interposer): a cold pool's acquire/begin = the connect above; a warm pool's: 0 events.",
6809 },
6810 AlsoEffect {
6811 krate: &["sqlx"],
6812 on: AlsoOn::Method("SqlitePoolOptions", &["acquire", "begin", "begin_with"]),
6813 effects: &["Env", "Fs", "Rand"],
6814 fs_kind: &[],
6815 row: "R1623",
6816 evidence: "EXECUTED (sqlite, interposer): a cold pool's acquire/begin = the connect above; a warm pool's: 0 events.",
6817 },
6818];
6819
6820/// The effects `(crate_name, path)` performs BESIDE its primary `classify` answer — see [`ALSO_EFFECTS`]. Empty unless
6821/// `classify` charges the path (a second effect never stands alone: the κ ledger's "was this call classified" question
6822/// stays the primary's), and never contains the primary itself.
6823pub fn also_effects(crate_name: &str, path: &str) -> Vec<&'static str> {
6824 let Some(primary) = classify(crate_name, path) else { return Vec::new() };
6825 let mut out: Vec<&'static str> = Vec::new();
6826 for row in ALSO_EFFECTS {
6827 if row.krate.contains(&crate_name) && row.on.matches(path) {
6828 for e in row.effects {
6829 if *e != primary && !out.contains(e) {
6830 out.push(e);
6831 }
6832 }
6833 }
6834 }
6835 out
6836}
6837
6838/// SOUNDNESS R1617(4) — the second effects of EVERY `AlsoOn::Method` row of `crate_name` that names `method`, whatever its
6839/// type. For a call whose receiver type is a GUESS — a dependency builder chain the scan types as its ROOT (`c.get(u).send()`
6840/// reaches here as `Client::send`, the step's real type `RequestBuilder` being unknown) — and whose primary the rule tables
6841/// already charge on that guessed path: the step's type is one of the crate's, so the union over the crate's rows for the
6842/// method over-approximates the row that applies. Empty when no row names the method.
6843pub fn also_effects_by_method(crate_name: &str, method: &str) -> Vec<&'static str> {
6844 let mut out: Vec<&'static str> = Vec::new();
6845 for row in ALSO_EFFECTS {
6846 if row.krate.contains(&crate_name) && matches!(row.on, AlsoOn::Method(_, ms) if ms.contains(&method)) {
6847 for e in row.effects {
6848 if !out.contains(e) {
6849 out.push(e);
6850 }
6851 }
6852 }
6853 }
6854 out
6855}
6856
6857/// The §2 `fs` direction of the second `Fs` [`also_effects`] charges for this path: the union of the matching rows'
6858/// `fs_kind`, or empty (direction unknown — record the poison marker) if any matching `Fs` row declares none.
6859pub fn also_fs_kind(crate_name: &str, path: &str) -> Vec<&'static str> {
6860 let mut out: Vec<&'static str> = Vec::new();
6861 for row in ALSO_EFFECTS {
6862 if row.krate.contains(&crate_name) && row.on.matches(path) && row.effects.contains(&"Fs") {
6863 if row.fs_kind.is_empty() {
6864 return Vec::new();
6865 }
6866 for k in row.fs_kind {
6867 if !out.contains(k) {
6868 out.push(k);
6869 }
6870 }
6871 }
6872 }
6873 out
6874}
6875
6876/// Every effect a `classify`-charged call performs: the primary first, then [`also_effects`]. `None` when `classify` is.
6877pub fn classify_all(crate_name: &str, path: &str) -> Option<Vec<&'static str>> {
6878 let primary = classify(crate_name, path)?;
6879 let mut v = vec![primary];
6880 v.extend(also_effects(crate_name, path));
6881 Some(v)
6882}
6883
6884/// The coverage-gate rendering of [`classify_all`]: `Rand` for a one-effect rule (the pre-R1612 column, unchanged), and
6885/// `Rand+Clock` — primary, then the second effects in table order — for a rule with more.
6886pub fn classify_rendered(crate_name: &str, path: &str) -> Option<String> {
6887 classify_all(crate_name, path).map(|v| v.join("+"))
6888}
6889
6890/// Does a SECOND effect of this name have a locator surface (§2 `hosts`/`fs`/`cmds`/`tables`) that the call does not
6891/// show? Then the charging site marks it `incomplete`: the call's argument is the PRIMARY's locator, never the second
6892/// effect's, so without the mark a benign sibling literal would certify it (`allow Fs /etc/hosts` over a function that
6893/// also stats `PATH`). Effects with no surface (`Env`/`Clock`/`Rand`/`Log`/`Ipc`/`Clipboard`) need nothing.
6894pub fn also_hides_locator(eff: &str) -> bool {
6895 matches!(eff, "Fs" | "Net" | "Llm" | "Exec" | "Db")
6896}
6897
6898
6899/// SOUNDNESS R330 — IS THIS `hash_password` CALL PROVABLY THE EXPLICIT-SALT ONE (password-hash 0.5.x)?
6900///
6901/// **THE DEFECT THIS EXISTS FOR, and it was mine, introduced by R318's fix the previous day.** R318
6902/// correctly found that `hash_password` mints OS entropy — at password-hash **0.6.x**, where
6903/// `fn hash_password(&self, password)` (`lib.rs:107`) has the body `let salt = try_generate_salt()?;`.
6904/// At **0.5.0** (`traits.rs:33`) and argon2 **0.5.3** (`lib.rs:572`) the identical name is
6905/// `fn hash_password(&self, password, salt)`: the caller supplies the salt and the call is deterministic
6906/// recomputation — PURE. So the `ends_with`-not-`contains` guard R318 relied on was keyed on the 0.6.x
6907/// name, and at 0.5.x the explicit-salt call IS named `hash_password`. Measured: two calls with the same
6908/// stored salt produce byte-identical hashes, and all four 0.5.x spellings were charged `Rand`.
6909///
6910/// **Why arity and not the resolved version.** R330's remedy offered both. The version is only knowable
6911/// where a lockfile resolves, and `calibration_exceeded` is ONE-SIDED — it fires when resolved > ceiling,
6912/// so it can never see a rule that mis-describes an OLDER release, which is exactly this direction. The
6913/// arity is written at the call site, needs no lockfile, and distinguishes the two signatures exactly.
6914///
6915/// **The direction is stated so it cannot drift:** this returns true only for the shape it can PROVE is
6916/// explicit-salt. An unrecorded arity (`argc == 0`, every synthesized edge and every pre-R330 cache
6917/// entry) returns false and the call stays charged. Narrowing a sound over-approximation by DENYLIST,
6918/// never by allowlist — the family rule.
6919pub fn password_hash_explicit_salt(crate_name: &str, path: &str, argc: u8, method: bool) -> bool {
6920 if !matches!(crate_name, "argon2" | "scrypt" | "pbkdf2" | "password_hash")
6921 || !path.ends_with("::hash_password")
6922 {
6923 return false;
6924 }
6925 if argc == 0 {
6926 return false; // NOT RECORDED — see `Call::argc`. Unknown charges.
6927 }
6928 // `syn` hands a method call its args WITHOUT the receiver and a UFCS `Trait::method(&recv, ..)`
6929 // WITH it, so normalize to the non-receiver count before comparing: 0.5.x is `(password, salt)` = 2,
6930 // 0.6.x is `(password)` = 1.
6931 let non_receiver = if method { argc } else { argc - 1 };
6932 let hit = non_receiver == 2;
6933 // §E1 HIT COUNTER, the `CANDOR_ALIAS_DEBUG` pattern. A refinement that narrows a charge must be
6934 // able to prove it REACHED real code, because "nothing moved" and "the branch never ran" print the
6935 // same corpus A/B. This is what `bin/corpus-ab.py --mark` counts.
6936 if hit && candor_report::env::trace("CANDOR_ALIAS_DEBUG") {
6937 eprintln!("R330EXPLICITSALT {path} argc={argc} method={method}");
6938 }
6939 hit
6940}
6941
6942/// ⟨0.29⟩ HOW MANY LEADING ARGUMENTS OF AN `Fs` PATH-TAKING CALL ARE PATHS.
6943///
6944/// **THE DEFECT THIS EXISTS FOR.** The literal harvester took the first string literal found ANYWHERE in
6945/// the argument list, so `fs::write(user_path, "/tmp/lit")` published `paths: ["/tmp/lit"]` — the BYTES
6946/// BEING WRITTEN — as the destination surface, and `allow Fs /tmp/lit` certified a write to an
6947/// attacker-controlled path at exit 0. Measured on candor-scan and candor-ts; candor-java and
6948/// candor-swift read the path POSITION and fail closed correctly. Removing the sibling literal made the
6949/// same call fail closed, which is what identified the mechanism: any literal in the call, in any
6950/// position, defeated the runtime-path incompleteness marker.
6951///
6952/// So the surface is read from the PATH POSITIONS and nowhere else, and every one of them must be a
6953/// literal for the surface to be complete. Two-path operations are the reason this is an arity and not a
6954/// boolean: `fs::copy("/safe", user_path)` has a literal at position 0 and still writes somewhere
6955/// nobody can see, so requiring only position 0 would leave the identical hole one argument along.
6956pub fn fs_path_arity(leaf: &str) -> usize {
6957 match leaf {
6958 "copy" | "rename" | "hard_link" | "soft_link" | "symlink" | "symlink_file" | "symlink_dir" => 2,
6959 _ => 1,
6960 }
6961}
6962
6963/// SOUNDNESS R383 — THE METHOD FORMS WHOSE PATH IS AN ARGUMENT, not the receiver.
6964///
6965/// `scan.rs`'s Fs masking arm reads `!c.method && is_fs_path_arg(leaf)`. The `!c.method` gate exists for
6966/// a real reason — the path-STAT methods (`p.metadata()`, `p.exists()`) carry the path as the RECEIVER,
6967/// so masking them would be wrong — but it excluded EVERY method form, and `OpenOptions::open(path)` is
6968/// a method whose path IS the argument. Measured, with the instrument calibrated first: a function doing
6969/// `File::open("/etc/hostname")` beside `OpenOptions::new().read(true).open(caller_path)` reported
6970/// `paths:['/etc/hostname'] incomplete:NONE`; `deny Fs` exited 1 and **`allow Fs /etc/hostname` exited
6971/// 0** — a policy saying "only /etc/hostname" going green over a caller-supplied path. The FREE-fn
6972/// spelling of the same call is the isolating control: it marks incomplete and fails closed.
6973///
6974/// **`argc` does not discriminate; the RECEIVER TYPE does.** Instrumented, the four shapes are
6975/// `OpenOptions::open` (argc 1, path IS the arg), `File::read_to_string` and `File::write` (argc 1,
6976/// BUFFER arg) and `Path::exists` (argc 0, path is the receiver).
6977///
6978/// **The family is from a 1,545-crate sweep, not from the one spelling that was found (R346):** the only
6979/// path-taking METHOD pairs in the registry are `OpenOptions::open` (69 std + 2 tokio + 9
6980/// `cap_primitives`) and `DirBuilder::create` (5 std + tokio). Every other method on those same receivers
6981/// is a builder modifier (`read`/`write`/`truncate`/`mode`) or a combinator (`map`/`map_err`/`or_else`/
6982/// `with_context`), and `std::fs::File`'s 267 method hits are all use-verbs whose path was fixed at open.
6983///
6984/// Matched on the TAIL of the resolved call path, so std, tokio, `cap_primitives` and `fs_err` are all
6985/// covered by one rule without naming a crate. **Stays an ALLOWLIST:** R379 built the denylist inversion
6986/// for the Net twin and the corpus refused it, and here a denylist over method forms would mask all 267
6987/// `File` use-verbs and every combinator on a whole-crate-Fs receiver.
6988///
6989/// **SOUNDNESS R417 — THE CAPABILITY-`Dir` FAMILY WAS ABSENT, AND IT IS A LIVE GATE BYPASS.**
6990/// `cap_std::fs::Dir` (and its `fs_utf8`/`cap_async_std` twins, and `openat::Dir`) is a whole second
6991/// filesystem API whose path is an ARGUMENT to a METHOD on a directory handle — `d.write(caller, …)`,
6992/// `d.open(caller)`, `d.remove_file(caller)`. MEASURED, with the free-fn spelling as the isolating
6993/// control in the same fixture:
6994///
6995/// ```text
6996/// control(caller) std::fs::write("/tmp/benign",…); std::fs::write(caller,…)
6997/// → paths:['/tmp/benign'] incomplete:['Fs'] allow Fs /tmp/benign → exit 1 ✔
6998/// mixed(d,caller) std::fs::write("/tmp/benign",…); d.write(caller,…)
6999/// → paths:['/tmp/benign'] incomplete:NONE allow Fs /tmp/benign → exit 0 ✘
7000/// ```
7001///
7002/// The call path resolves in full (`cap_std::fs::Dir::write`), so this is not a resolution gap — the
7003/// guard simply had two names in it. `alone(d,caller)` confirms the call IS classified `Fs`: the
7004/// effect was seen, the locator was not, and nothing said so.
7005///
7006/// **WHY THIS HALF IS A DENYLIST WHILE THE REST OF THE GUARD IS AN ALLOWLIST**, which is not a
7007/// contradiction but the point: the allowlist above exists because the receiver is UNKNOWN, so masking
7008/// by default would swallow `File`'s 267 use-verbs. Here the receiver is KNOWN to be a directory
7009/// capability, and on that receiver essentially every method names a path — 24 of the 49 declared on
7010/// `cap_std::fs::Dir` are already `is_fs_path_arg` leaves, and most of the remaining 25 (`open_with`,
7011/// `open_dir`, `is_file`, `is_dir`, `symlink_contents`, `read_link_contents`, `create_dir_with`,
7012/// the `bind_unix_*`/`connect_unix_*` socket-path verbs) take one too. An allowlist here would be the
7013/// R346→R348 hand-list vein by construction: every name I failed to write down is a silent bypass, and
7014/// R399 settled that an under-catch on this guard is a BROKEN GATE, not a missed mask.
7015///
7016/// So the default on a `Dir` receiver is MASK, and the exceptions are the handful of methods that
7017/// provably take no path: handle-to-handle conversions and the iterator/`self`-only verbs. Being wrong
7018/// about one of those costs an over-mask on a rare API (fails CLOSED, disclosed); being wrong the other
7019/// way is the exit-0 above. [[candor-denylist-over-allowlist]] — say which direction it fails in first.
7020///
7021/// Scoped on the RECEIVER SEGMENT being exactly `Dir`, so `cap_std`, `cap_std::fs_utf8`,
7022/// `cap_async_std` and `openat` are covered by one rule without naming a crate — the same tail-matching
7023/// discipline as the two names above. A non-filesystem type that happens to be called `Dir` would be
7024/// over-masked, which fails closed.
7025/// SOUNDNESS R414 / SPEC ⟨0.37⟩ — **a RECEIVER-form path stat names its destination, and every engine
7026/// in this family was silent on it.** `p.exists()`, `p.metadata()`, `p.canonicalize()` reach the
7027/// filesystem against the path they are invoked ON; the locator is the RECEIVER, not an argument.
7028/// MEASURED before the clause was written, one variable, a benign allowed literal beside a
7029/// caller-chosen path:
7030///
7031/// ```text
7032/// fn f(p: &Path) { fs::write("/tmp/benign", b"x"); p.exists(); }
7033/// -> paths:['/tmp/benign'] incomplete:NONE allow Fs /tmp/benign -> exit 0, "nothing hidden"
7034/// ```
7035///
7036/// The ARGUMENT spelling of the identical reach (`fs::metadata(p)`) was already marked, which is what
7037/// makes this a drift rather than a policy: `is_fs_path_arg` deliberately excluded the method form
7038/// because its receiver was assumed to be an OPEN HANDLE. That assumption holds for `File` and is
7039/// false for `Path` — a `Path` is a path VALUE, and a stat on it is `Fs` reaching that value.
7040///
7041/// **NO CARVE-OUT LIST, and that is structural rather than lucky.** The pure `Path` algebra —
7042/// `join`, `parent`, `file_name`, `extension`, `to_str`, `display`, `starts_with`, `to_path_buf`,
7043/// `as_path` — is NOT classified `Fs` (measured: a function calling all nine reports `fs:['write']`
7044/// from an unrelated sibling call and nothing else), so it never reaches the branch that consults
7045/// this predicate. The carve-out falls out of the classification, exactly as it does on the java
7046/// side, instead of being a list that must be kept in sync with `std`.
7047///
7048/// Matched on the RECEIVER SEGMENT so `std::path` and the `camino` mirrors are covered by one rule
7049/// without naming a crate. Being generous here is the SAFE direction: the predicate is only ever
7050/// consulted for a call the classifier ALREADY resolved to `Fs`, so an extra receiver name can at
7051/// worst over-mask (fails closed, disclosed), while a missing one is the exit-0 above — R399's
7052/// ruling that an under-catch on a masking guard is a broken gate, not a missed mask.
7053pub fn is_fs_receiver_locator(call_path: &str) -> bool {
7054 let mut segs = call_path.rsplit("::");
7055 let (Some(_leaf), Some(recv)) = (segs.next(), segs.next()) else { return false };
7056 matches!(recv, "Path" | "PathBuf" | "Utf8Path" | "Utf8PathBuf")
7057}
7058
7059/// SOUNDNESS R460 / SPEC ⟨0.37⟩ — **the Exec twin of `is_fs_receiver_locator`: a spawn's locator is
7060/// its RECEIVER, and rust was silent on it.** `is_cmd_naming_method` marks the surface incomplete at
7061/// `Command::new(<runtime value>)`, which covers every spelling that NAMES the program inside the
7062/// function. It cannot cover the spelling that names it somewhere else:
7063///
7064/// ```text
7065/// fn masked(cmd: &mut Command) { Command::new("git").status(); cmd.spawn(); }
7066/// -> cmds:['git'] incomplete:NONE allow Exec git -> exit 0, "nothing hidden"
7067/// ```
7068///
7069/// MEASURED on the pre-fix binary, one variable — deleting the benign `Command::new("git")` line, and
7070/// nothing else, turns the same gate to exit 1. The real instance is tokio's own
7071/// `process::unix::spawn_child(cmd: &mut std::process::Command) { cmd.spawn()? }`, production source,
7072/// charged `Exec` with no `cmds` and no `incomplete`.
7073///
7074/// **A DENYLIST, and the direction is the point.** On a `Command` receiver essentially every method
7075/// that is not a documented BUILDER modifier reaches the boundary (`spawn`, `status`, `output`, `exec`,
7076/// and every async / pty mirror of them), so an allowlist of terminal names would be the R346→R348
7077/// hand-list vein by construction: each name not written down is a silent bypass. The carve-out is
7078/// `is_cmd_builder_method` — the SAME list the head-refinement already trusts, not a second copy of it
7079/// (§G, ask the authority) — plus the `get_*` getters that list's own doc-block names. Being wrong
7080/// about a carve-out costs an over-mask on a configure-only call (fails closed, disclosed); being
7081/// wrong the other way is the exit-0 above.
7082///
7083/// Scoped on the RECEIVER SEGMENT being exactly `Command`, so `std::process`, `tokio::process`,
7084/// `async_process` and the `std::os::unix` extension trait are one rule without naming a crate — the
7085/// same tail-matching discipline as `is_fs_receiver_locator`. The predicate is only ever consulted for
7086/// a call the classifier ALREADY resolved to `Exec`, so a non-spawning type that happens to be called
7087/// `Command` (clap's builder) is unreachable unless something already charged it Exec.
7088///
7089/// The DETERMINED receiver does not arrive here: the collector resolves a builder chain rooted at
7090/// `Command::new("git")` — inline or through a `let` binding — into `str_arg`, so only a receiver whose
7091/// program was never named in this function reaches the mask. That split is this fix's over-charge
7092/// control, and it is what keeps the dominant spelling certifiable.
7093pub fn is_exec_receiver_locator(call_path: &str) -> bool {
7094 let mut segs = call_path.rsplit("::");
7095 let (Some(leaf), Some(recv)) = (segs.next(), segs.next()) else { return false };
7096 // SOUNDNESS R808 — AND ITS EXTENSION TRAIT. The deep engine resolves `cmd.exec()` to the TRAIT method
7097 // `std::os::unix::process::CommandExt::exec`, not to `Command::exec`, so a receiver test on the
7098 // literal segment `Command` let the deep engine certify a caller-supplied `exec` while `candor-scan`
7099 // (which forms `Command::exec` from the typed receiver) flagged it — the R808 port's own boundary,
7100 // measured on the port's fixture one method over. `CommandExt`'s other members (`uid`, `gid`,
7101 // `groups`, `pre_exec`, `arg0`, `process_group`) are already `is_cmd_builder_method` carve-outs.
7102 matches!(recv, "Command" | "CommandExt") && !is_cmd_builder_method(leaf) && !leaf.starts_with("get_")
7103}
7104
7105pub fn is_fs_path_arg_method(call_path: &str) -> bool {
7106 if call_path.ends_with("OpenOptions::open") || call_path.ends_with("DirBuilder::create") {
7107 return true;
7108 }
7109 let mut segs = call_path.rsplit("::");
7110 let (Some(leaf), Some(recv)) = (segs.next(), segs.next()) else { return false };
7111 if recv != "Dir" {
7112 return false;
7113 }
7114 // The no-path exceptions on a directory capability: handle↔handle conversions, the entry
7115 // iterators, and the verbs whose only operand is `self`. Everything else on a `Dir` names a path.
7116 //
7117 // EVERY NAME HERE WAS CHECKED AGAINST ITS SIGNATURE, because a wrong entry here is not a precision
7118 // loss, it is the exit-0 above. `read_dir` was in this list on the first cut on the reasonable-
7119 // sounding grounds that `entries()` is the self-only form — and `cap_std::fs::Dir::read_dir` is
7120 // `fn read_dir<P: AsRef<Path>>(&self, path: P)`, so that entry was a bypass I had just written.
7121 // Adding a name here requires reading its signature, not recognising it.
7122 !matches!(
7123 leaf,
7124 "try_clone"
7125 | "into_std_file"
7126 | "from_std_file"
7127 | "into_std"
7128 | "from_std"
7129 | "entries"
7130 | "entries_utf8"
7131 | "dir_metadata"
7132 | "reopen_dir"
7133 | "remove_open_dir"
7134 | "remove_open_dir_all"
7135 | "open_parent_dir"
7136 | "rewind"
7137 | "as_raw_fd"
7138 | "as_fd"
7139 )
7140}
7141
7142/// The masking guard (AS-EFF-008), the `Db` analog of `is_net_establishing`: whether a `Db`-classified
7143/// call takes the raw SQL QUERY as a string argument (so a missing literal leaves the table
7144/// structurally INVISIBLE — a runtime-built query — and the surface is incomplete, fail-closed). An
7145/// ALLOWLIST of the SQL-string-bearing execution/prepare verbs, the SAFE direction: a
7146/// build-then-execute terminal that takes NO SQL string (sqlx/diesel/sea_orm `fetch*`/`load*`/`first`/
7147/// `all`/`one`/`stream`, the document-store `find*`/`insert*`/…), and a non-query op (`connect`/
7148/// `open`/`acquire`/`begin`/`commit`/`ping`/`get_conn`), are NOT here — their query is built
7149/// structurally (never a maskable string literal) so a missing literal must not false-positive.
7150/// Under-catching an unusual query verb is a missed mask (sound-with-disclosure), ~~never a broken
7151/// gate~~ — **FALSE, SOUNDNESS R399**, and R386 three lines below is this guard's own counter-example.
7152/// `mysql`'s `query_drop`/`query_iter` are that crate's PRIMARY API, take `Q: AsRef<str>`, and were
7153/// absent: `allow Db users` exited 0 over `c.query_drop(caller_sql)` beside one literal query, with the
7154/// verb-only control exiting 1. The arg is the method leaf (the path's last segment).
7155pub fn is_db_query_arg(leaf: &str) -> bool {
7156 // SOUNDNESS R386 — THE `sqlite3_*` C API TAKES SQL AND WAS ABSENT, so a benign sibling literal
7157 // certified arbitrary caller-supplied SQL. `classify` in this same crate maps `sqlite3_exec`/
7158 // `sqlite3_step`/`sqlite3_prepare*` to `Db`; this list had none of them, while candor-swift covers
7159 // the whole family by prefix — a cross-engine divergence, which is how it was found.
7160 //
7161 // MEASURED, calibrated first (`deny Db` exits 1): `conn.execute("INSERT INTO users (id) VALUES (1)",
7162 // [])` beside `sqlite3_exec(db, caller_sql, …)` reported `tables:['users'] incomplete:NONE` and
7163 // **`allow Db users` exited 0**. R383 recorded this gap as "REAL BUT NOT LIVE" on the grounds that a
7164 // C-string literal never reaches `tables` — true of an ISOLATED function, and the wrong shape: the
7165 // literal does not have to come from the masked call, only from the same FUNCTION.
7166 //
7167 // ONLY THE SQL-BEARING SPELLINGS. `sqlite3_step`/`_close`/`_serialize`/`_deserialize` carry no query,
7168 // and `sqlite3_open*` takes a FILENAME — masking that would claim a Db surface for an Fs locator,
7169 // which is the over-mask this allowlist exists to avoid. Direction stays an ALLOWLIST for the reason
7170 // R379 measured: a denylist over a whole-crate-Db crate masks every lifecycle verb in it.
7171 if matches!(
7172 leaf,
7173 "sqlite3_exec"
7174 | "sqlite3_prepare"
7175 | "sqlite3_prepare_v2"
7176 | "sqlite3_prepare_v3"
7177 | "sqlite3_prepare16"
7178 | "sqlite3_prepare16_v2"
7179 | "sqlite3_prepare16_v3"
7180 // R386's OWN BOUNDARY DEFECT, found by a release panel: `sqlite3_get_table(db, zSql, …)`
7181 // takes SQL as its second argument and `classify` maps it to `Db`, and the first cut of
7182 // this list — which cites R346 — omitted it. Two of the family's SQL-bearing spellings
7183 // written down out of three. Reach on the local registry is ZERO call sites (only
7184 // declarations in `libsqlite3-sys` and `sqlite-wasm-rs`), so this is latent, not live.
7185 | "sqlite3_get_table"
7186 ) {
7187 return true;
7188 }
7189 matches!(
7190 leaf,
7191 "execute"
7192 | "execute_batch"
7193 | "execute_unprepared"
7194 | "batch_execute"
7195 | "simple_query"
7196 | "query"
7197 | "query_one"
7198 | "query_opt"
7199 | "query_raw"
7200 | "query_row"
7201 | "query_map"
7202 // SOUNDNESS R399 — `mysql`'s OWN PRIMARY API was absent, and this is a live gate bypass rather
7203 // than the "missed mask" the struck sentence above promised. All four take `Q: AsRef<str>`
7204 // (`mysql-28.0.2/src/conn/queryable.rs:29,50,126`), so the SQL is a maskable string argument of
7205 // THIS call, which is the exact condition this list encodes. Measured: `allow Db users` exits 0
7206 // over `c.query_drop(caller_sql)` beside one literal query; the control, the same fixture with
7207 // the verb changed to `query`, exits 1.
7208 | "query_drop"
7209 | "query_iter"
7210 | "query_first"
7211 | "query_fold"
7212 // postgres COPY: the statement is a SQL string argument (`postgres-0.19.14/src/client.rs:492,520`).
7213 | "copy_in"
7214 | "copy_out"
7215 | "query_and_then"
7216 | "query_typed"
7217 | "query_all"
7218 | "prepare"
7219 | "prepare_typed"
7220 | "prepare_cached"
7221 | "exec"
7222 | "exec_first"
7223 | "exec_iter"
7224 | "exec_map"
7225 | "exec_fold"
7226 | "exec_drop"
7227 | "exec_batch"
7228 | "prep"
7229 | "run_command"
7230 )
7231}
7232
7233/// THE AS-EFF-008 MASKING GUARD, ONE AUTHORITY FOR BOTH RUST ENGINES. SOUNDNESS R806 / R808.
7234///
7235/// The question: this call carries a surface effect `eff` and NO literal locator was captured for it —
7236/// is its locator (host, program, path, SQL) therefore a RUNTIME value the gate cannot see? If so the
7237/// caller marks the surface `incomplete`, so a benign sibling literal cannot certify it.
7238///
7239/// **WHY ONE FUNCTION.** Both rust engines asked this question, each with its own copy of the
7240/// disjunction over the `is_*` predicates below, and the copies drifted: R460's receiver-locator arm and
7241/// R383's `OpenOptions::open` arm reached `candor-scan` and never the deep engine (R808). A rule added
7242/// here now reaches both, which is the only way this class stops recurring one crate over.
7243///
7244/// `leaf` is the call's own method/function name; `path` the resolved call path; `method` whether the
7245/// call is written in method form on a receiver (so a path-stat `p.exists()` is not mistaken for an
7246/// argument-form `fs::metadata(p)`, and the receiver-locator arms can fire). `crate_name` is the
7247/// resolved crate, used only to scope the table-derived FFI arms.
7248///
7249/// **THE DIRECTION, stated so it cannot drift:** a wrong `true` over-masks — the gate fails CLOSED and
7250/// says so; a wrong `false` is the benign-sibling-literal bypass, a broken gate (R399). Every arm that
7251/// reads a `classify` table below reads it with a NO-LOCATOR DENYLIST, so a leaf added to that table is
7252/// masked until someone reads its signature and says otherwise.
7253pub fn masks_locator(eff: &str, crate_name: &str, path: &str, leaf: &str, method: bool) -> bool {
7254 let by_list = match eff {
7255 "Net" => is_net_establishing(leaf),
7256 // SOUNDNESS R808 — the receiver arm is NOT gated on method form. `Command::spawn(cmd)` (UFCS)
7257 // runs the same caller-supplied program as `cmd.spawn()`, and with the gate BOTH engines
7258 // certified it at exit 0 on the unit and its caller. In call form the receiver is argument 0;
7259 // neither engine resolves a determined argument-0 command, so a UFCS spawn on a command built
7260 // in the same function over-masks (fails closed) — the rare spelling paying for the silent one.
7261 "Exec" => is_cmd_naming_method(leaf) || is_exec_receiver_locator(path),
7262 "Fs" => {
7263 (!method && is_fs_path_arg(leaf))
7264 || (method && is_fs_path_arg_method(path))
7265 || (method && is_fs_receiver_locator(path))
7266 }
7267 "Db" => is_db_query_arg(leaf),
7268 _ => false,
7269 };
7270 by_list || masks_locator_by_table(eff, crate_name, path, leaf)
7271}
7272
7273/// SOUNDNESS R806 — THE TABLE-DERIVED HALF OF `masks_locator`, separate only so a reach probe can ask
7274/// the very function the guard asks (`R806MASK` in candor-scan) rather than a copy of it.
7275///
7276/// Each hand list in `masks_locator` was SHORT against the `classify` table that charges the effect:
7277/// `is_fs_path_arg` held 28 names while `LIBC_FS` charges 92, `is_cmd_naming_method` held `new`/`cmd`
7278/// while `LIBC_EXEC` names `system`/`popen`/`exec*`/`posix_spawn*`, `is_net_establishing` lacked
7279/// `getaddrinfo`, and `is_db_query_arg` lacked `prepare_typed_cached` from `PG_DB_VERBS`. EXECUTED on
7280/// each: a caller's file really unlinked, a caller's shell command really run, a real resolver lookup,
7281/// all under `allow <E> in <unit> <benign literal>` at exit 0 on the unit AND its caller.
7282///
7283/// Adding the missing names would leave two tables to drift again the next time one grows — the shape
7284/// swift closed as R787 and java as R794, both by derivation. So these arms READ the charging table and
7285/// subtract a signature-checked NO-LOCATOR denylist: a leaf added to the table is masked by default.
7286pub fn masks_locator_by_table(eff: &str, crate_name: &str, path: &str, leaf: &str) -> bool {
7287 let syscall = is_syscall_crate(crate_name);
7288 match eff {
7289 "Net" => {
7290 syscall
7291 && LIBC_NET.contains(&leaf)
7292 && !LIBC_NET_NO_DESTINATION.contains(&leaf)
7293 // `rustix::net::sendmsg(socket, iov, control, flags)` takes NO address — rustix spells
7294 // the addressed forms `sendmsg_addr`/`sendmsg_v4`/`sendmsg_v6`/`sendmsg_unix` (not in
7295 // `LIBC_NET`, so unclassified). libc's and nix's `sendmsg` DO carry one (`msg_name` /
7296 // `addr: Option<&S>`), which is why this is scoped to the crate rather than the leaf.
7297 // Measured on the A/B: x11rb's `DefaultStream::do_write` over its connected X11 socket
7298 // was the only reach, an over-mask, removed here.
7299 && !(crate_name == "rustix" && leaf == "sendmsg")
7300 }
7301 "Exec" => syscall && LIBC_EXEC.contains(&leaf) && !LIBC_EXEC_NO_PROGRAM.contains(&leaf),
7302 "Fs" => syscall && LIBC_FS.contains(&leaf) && !LIBC_FS_NO_PATH.contains(&leaf),
7303 "Db" => {
7304 (matches!(crate_name, "postgres" | "tokio_postgres" | "deadpool_postgres" | "rusqlite")
7305 && PG_DB_VERBS.iter().any(|v| path.ends_with(v))
7306 && !PG_DB_VERBS_NO_SQL.iter().any(|v| path.ends_with(v)))
7307 // `sqlx_core`'s `Executor::prepare_with(sql, params)` — the one SQL-TEXT verb in that
7308 // crate's Db rule that the leaf list lacked (`prepare` is there). The rest of that rule
7309 // (`fetch*`/`execute*` on a `Query`) takes an EXECUTOR at argument 0, so deriving the
7310 // whole rule would mask every `query(lit).fetch_all(&pool)`; it is a named member
7311 // rather than a derivation for that reason.
7312 || (crate_name == "sqlx_core" && leaf == "prepare_with")
7313 }
7314 _ => false,
7315 }
7316}
7317
7318/// Map a cap-std capability *type* to the effect it authorises. Holding one of these
7319/// (e.g. `&Dir`) is the real, unforgeable right to perform that effect — so candor
7320/// treats it as a declared capability, exactly like its own `&Fs` token.
7321pub fn capstd_cap(crate_name: &str, type_name: &str) -> Option<&'static str> {
7322 if !crate_name.starts_with("cap_") {
7323 return None;
7324 }
7325 Some(match type_name {
7326 "Dir" => "Fs",
7327 "TcpListener" | "TcpStream" | "UdpSocket" | "Pool" => "Net",
7328 "UnixListener" | "UnixStream" | "UnixDatagram" => "Ipc",
7329 "SystemClock" | "MonotonicClock" => "Clock",
7330 _ => return None,
7331 })
7332}
7333
7334/// Table names a SQL string literal STATICALLY reaches — the `Db` analog of the `Net` host /
7335/// `Exec` command / `Fs` path literal surface (feeds `allow Db in <scope> <table>…`, AS-EFF-008).
7336/// Conservative by construction, because a wrong capture here would FABRICATE: the string must
7337/// open with a SQL statement keyword, and only identifiers in table position are taken —
7338/// `FROM`/`JOIN` anywhere, `INTO` anywhere, statement-leading `UPDATE`/`TRUNCATE`, and
7339/// `TABLE` (create/drop/alter), skipping `ONLY`/`IF NOT EXISTS`. `UPDATE` mid-statement is
7340/// deliberately ignored (`FOR UPDATE SKIP LOCKED` must not yield a table "skip"). A
7341/// dynamically-built query yields nothing — the gate's opaque case — never a guess.
7342/// Output is lower-cased, quote/backtick-stripped, `schema.table` kept qualified, deduped.
7343/// SPEC §2 pins this algorithm token-for-token across engines; the cross-impl vector battery
7344/// (candor-spec conformance/tables/vectors.json, run.sh Part 4b) enforces the JVM/TS mirrors.
7345pub fn tables_in_sql(sql: &str) -> Vec<String> {
7346 const STMT: &[&str] =
7347 &["select", "insert", "update", "delete", "create", "drop", "alter", "truncate", "merge", "replace", "with"];
7348 // Tokens that can FOLLOW a table-introducing keyword without being a table.
7349 const SKIP: &[&str] = &["only", "if", "not", "exists", "table"];
7350 // Identifier-position tokens that are grammar, not a table (subqueries, locking clauses…).
7351 const STOP: &[&str] = &[
7352 "select", "set", "where", "values", "on", "using", "group", "order", "by", "limit",
7353 "returning", "as", "inner", "outer", "left", "right", "cross", "lateral", "natural",
7354 "union", "all", "distinct", "case", "when", "null", "default", "skip", "nowait", "of",
7355 "from", "join", "into", "update", "delete", "insert",
7356 ];
7357 // `,` survives as its OWN token (not a space): it's what lets `FROM t1, t2` continue the table
7358 // list without fabricating from other comma-ridden positions (column lists, ON clauses).
7359 let cleaned: String = sql
7360 .to_lowercase()
7361 .chars()
7362 .flat_map(|c| match c {
7363 '(' | ')' | ';' => vec![' '],
7364 ',' => vec![' ', ',', ' '],
7365 _ => vec![c],
7366 })
7367 .collect();
7368 let toks: Vec<&str> = cleaned.split_whitespace().collect();
7369 let Some(first) = toks.first() else { return Vec::new() };
7370 if !STMT.contains(first) {
7371 return Vec::new(); // not SQL — nothing to certify, nothing fabricated
7372 }
7373 let ident = |t: &str| -> Option<String> {
7374 let t = t.trim_matches(|c| matches!(c, '"' | '`' | '\''));
7375 let mut chars = t.chars();
7376 let ok_first = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
7377 let ok_rest = t.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '$' | '"' | '`'));
7378 (ok_first && ok_rest && !STOP.contains(&t)).then(|| t.replace(['"', '`'], ""))
7379 };
7380 let mut out: Vec<String> = Vec::new();
7381 let mut push = |t: Option<String>| {
7382 if let Some(t) = t {
7383 if !out.contains(&t) {
7384 out.push(t);
7385 }
7386 }
7387 };
7388 for (i, tok) in toks.iter().enumerate() {
7389 let table_pos = match *tok {
7390 "from" | "join" | "into" | "table" => true,
7391 // statement-leading only (see doc comment): `update t set …`, `truncate [table] t`.
7392 "update" | "truncate" => i == 0,
7393 _ => false,
7394 };
7395 if !table_pos {
7396 continue;
7397 }
7398 let mut j = i + 1;
7399 while j < toks.len() && SKIP.contains(&toks[j]) {
7400 j += 1;
7401 }
7402 let Some(next) = toks.get(j) else { continue };
7403 let Some(first) = ident(next) else { continue };
7404 push(Some(first));
7405 // Comma-ADJACENT continuation only: `FROM t1, t2, t3` takes all three, while an alias breaks
7406 // the chain (`FROM t1 a, t2` keeps just t1 — an under-report, never a guess: skipping an
7407 // alias to chase the comma would fabricate tables out of `INSERT INTO t (a, b)`'s column
7408 // list, whose parens are spaces by the time we tokenize).
7409 while j + 2 < toks.len() && toks[j + 1] == "," {
7410 let Some(more) = ident(toks[j + 2]) else { break };
7411 push(Some(more));
7412 j += 2;
7413 }
7414 }
7415 out
7416}
7417
7418#[cfg(test)]
7419mod tests {
7420 #[test]
7421 fn r1150_getrandom_error_values_draw_nothing_but_the_draws_do() {
7422 for p in ["getrandom::Error::eq", "getrandom::&Error::eq", "getrandom::Error::raw_os_error", "getrandom::Error::fmt",
7423 "getrandom::error::Error::new_custom", "getrandom::Error::UNSUPPORTED"] {
7424 assert_eq!(classify("getrandom", p), None, "{p}");
7425 }
7426 for p in ["getrandom::fill", "getrandom::fill_uninit", "getrandom::u32", "getrandom::u64", "getrandom::getrandom",
7427 "getrandom::SysRng::try_fill_bytes", "getrandom::getrandom_uninit"] {
7428 assert_eq!(classify("getrandom", p), Some("Rand"), "{p}");
7429 }
7430 }
7431 #[test]
7432 fn model_host_recognizes_known_providers_and_special_forms() {
7433 use super::is_model_host as m;
7434 // exact known hosts (case-insensitive), with/without a port
7435 assert!(m("api.openai.com"));
7436 assert!(m("API.OpenAI.com"));
7437 assert!(m("api.anthropic.com:443"));
7438 assert!(m("generativelanguage.googleapis.com"));
7439 assert!(m("api.mistral.ai"));
7440 assert!(m("api.cohere.ai"));
7441 assert!(m("api.cohere.com")); // BOTH cohere hosts
7442 assert!(m("api.groq.com"));
7443 assert!(m("api.together.xyz"));
7444 assert!(m("api.perplexity.ai"));
7445 assert!(m("openrouter.ai"));
7446 // a subdomain of a known host counts
7447 assert!(m("eu.api.openai.com"));
7448 // Ollama: :11434 on a LOOPBACK host only (max-review r3 — a remote host on 11434 is not Ollama)
7449 assert!(m("localhost:11434"));
7450 assert!(m("127.0.0.1:11434"));
7451 assert!(!m("ollama.internal:11434")); // a remote/internal service on 11434 is NOT a model host
7452 // Bedrock: the FIRST label is the model-inference service, not the substring "bedrock"
7453 assert!(m("bedrock-runtime.us-east-1.amazonaws.com"));
7454 assert!(m("bedrock-runtime.eu-west-1.amazonaws.com"));
7455 assert!(m("bedrock-agent-runtime.us-east-1.amazonaws.com"));
7456 // NOT model hosts (never guessed)
7457 assert!(!m("example.com"));
7458 assert!(!m("api.stripe.com"));
7459 assert!(!m("localhost:8080")); // a non-Ollama local port
7460 assert!(!m("s3.us-east-1.amazonaws.com")); // amazonaws but not bedrock
7461 assert!(!m("bedrock-backups.s3.amazonaws.com")); // an S3 bucket merely NAMED bedrock — not the runtime
7462 assert!(!m("bedrock.us-east-1.amazonaws.com")); // the Bedrock CONTROL plane — not model inference
7463 assert!(!m("openai.com.evil.com")); // suffix trick — not a subdomain of a known host
7464 }
7465
7466 #[test]
7467 fn model_sdk_crate_is_crate_level_no_method_gating() {
7468 use super::is_model_sdk_crate as s;
7469 assert!(s("async_openai"));
7470 assert!(s("aws_sdk_bedrockruntime"));
7471 assert!(s("ollama_rs"));
7472 assert!(s("langchain_rust"));
7473 assert!(!s("reqwest"));
7474 assert!(!s("aws_sdk_s3"));
7475 }
7476
7477 #[test]
7478 fn sql_table_extraction_is_conservative() {
7479 use super::tables_in_sql as t;
7480 assert_eq!(t("SELECT id FROM users WHERE x = 1"), vec!["users"]);
7481 assert_eq!(t("select * from ledger.entries e join customers c on c.id = e.cid"),
7482 vec!["ledger.entries", "customers"]);
7483 assert_eq!(t("INSERT INTO audit_log (a) VALUES (?1)"), vec!["audit_log"]);
7484 assert_eq!(t("UPDATE accounts SET v = ?"), vec!["accounts"]);
7485 assert_eq!(t("DELETE FROM sessions WHERE id = ?"), vec!["sessions"]);
7486 assert_eq!(t("CREATE TABLE IF NOT EXISTS cache (k TEXT)"), vec!["cache"]);
7487 assert_eq!(t("TRUNCATE TABLE staging"), vec!["staging"]);
7488 // FOR UPDATE locking clause must not yield a phantom table (mid-statement update ignored)
7489 assert_eq!(t("SELECT * FROM jobs FOR UPDATE SKIP LOCKED"), vec!["jobs"]);
7490 // a subquery in FROM position yields nothing for that position
7491 assert_eq!(t("SELECT * FROM (SELECT 1) q"), Vec::<String>::new());
7492 // not SQL -> nothing (never fabricate)
7493 assert_eq!(t("/tmp/some/path"), Vec::<String>::new());
7494 assert_eq!(t("hello world from nowhere"), Vec::<String>::new());
7495 // comma-ADJACENT continuation: a FROM list takes every table in the chain…
7496 assert_eq!(t("SELECT a FROM t1, t2, s.t3 WHERE x = 1"), vec!["t1", "t2", "s.t3"]);
7497 // …but an alias breaks it (under-report, never a guess)…
7498 assert_eq!(t("SELECT a FROM t1 a1, t2 WHERE x = 1"), vec!["t1"]);
7499 // …which is exactly what keeps a column list from fabricating (parens are spaces by now).
7500 assert_eq!(t("INSERT INTO t (a, b) VALUES (1, 2)"), vec!["t"]);
7501 // a subquery after the comma stops the chain too
7502 assert_eq!(t("SELECT a FROM t1, (SELECT 1) q"), vec!["t1"]);
7503 }
7504
7505 use super::*;
7506
7507 /// The routed-handle list's MEMBERSHIP RULE, enforced rather than merely documented.
7508 ///
7509 /// (a) Every routed type must have a whole-type rule that answers an ARBITRARY method on it.
7510 /// If it did not, routing it would hand the scanner a path the classifier drops — motion with no
7511 /// effect, and the silent under-report would survive the fix that was supposed to close it. The
7512 /// probe leaf is deliberately a name no carve-out lists, so it measures the BASE rule.
7513 ///
7514 /// (b) The named exclusions stay excluded. `ReadDir` cannot have a verb-precise rule written for it
7515 /// (its `next` is a syscall and is spelled like every other iterator's), and `Metadata`/`DirEntry`/
7516 /// `Permissions`/`FileType` are pure DATA under the coarse `std::fs::` prefix — routing any of them
7517 /// charges an effect for reading a field. `OpenOptions`/`DirBuilder` LEFT this list by the route the
7518 /// list's own comment prescribed: their type-keyed setter carve-outs were written first, so the
7519 /// whole-type rule under them now answers only for the terminal verb.
7520 #[test]
7521 fn std_effect_handles_have_a_whole_type_rule_and_exclude_the_pure_surfaces() {
7522 for ty in STD_EFFECT_HANDLES {
7523 let probe = format!("{ty}::__candor_probe_verb");
7524 assert!(
7525 classify("std", &probe).is_some(),
7526 "`{ty}` is routed into the classifier by receiver inference, but an arbitrary method \
7527 on it classifies to NOTHING — the routing cannot close any under-report"
7528 );
7529 assert!(is_std_effect_handle(ty), "the predicate must agree with the list");
7530 }
7531 for ty in [
7532 "std::fs::ReadDir",
7533 "std::fs::Metadata", "std::fs::DirEntry", "std::fs::Permissions", "std::fs::FileType",
7534 "std::path::Path", "std::path::PathBuf", "std::vec::Vec",
7535 ] {
7536 assert!(!is_std_effect_handle(ty),
7537 "`{ty}` has a pure surface the coarse prefix rules would charge — it must not be \
7538 routed as a handle (Path/PathBuf route through their own verb-precise carve-out)");
7539 }
7540 }
7541
7542 /// The pure read-backs on the ROUTED types, which is what makes (a) above safe: a whole-type rule
7543 /// answering every method is only acceptable because the genuinely-pure methods were carved out
7544 /// first. Receiver routing is a second door into these rules, so it must not re-fabricate.
7545 #[test]
7546 fn routed_std_handles_keep_their_pure_accessor_carve_outs() {
7547 for p in [
7548 "std::process::Command::get_program", "std::process::Command::get_args",
7549 "std::process::Command::get_envs", "std::process::Command::get_current_dir",
7550 "std::process::Child::id",
7551 "std::net::TcpStream::local_addr", "std::net::TcpStream::peer_addr",
7552 "std::net::TcpStream::nodelay", "std::net::TcpStream::ttl",
7553 "std::net::UdpSocket::take_error",
7554 "std::fs::File::as_raw_fd", "std::fs::File::into_raw_fd",
7555 "std::os::unix::net::UnixStream::as_raw_fd",
7556 // The OPTION-BUILDER setters (SPEC §1 ⟨0.32⟩): flags in a struct, no file named, nothing
7557 // opened. `create` is here as `OpenOptions`' setter and is a VERB on `DirBuilder` below —
7558 // one leaf, two answers, which is why these carve-outs are keyed on the TYPE.
7559 "std::fs::OpenOptions::new", "std::fs::OpenOptions::read", "std::fs::OpenOptions::write",
7560 "std::fs::OpenOptions::append", "std::fs::OpenOptions::truncate",
7561 "std::fs::OpenOptions::create", "std::fs::OpenOptions::create_new",
7562 "std::fs::OpenOptions::mode", "std::fs::OpenOptions::custom_flags",
7563 "std::fs::DirBuilder::new", "std::fs::DirBuilder::recursive", "std::fs::DirBuilder::mode",
7564 ] {
7565 assert_eq!(classify("std", p), None, "`{p}` is a pure read-back");
7566 }
7567 // …and the verbs on the same types still answer, or the carve-outs would have eaten the rule.
7568 for (p, want) in [
7569 ("std::process::Command::spawn", "Exec"),
7570 ("std::process::Child::wait", "Exec"),
7571 ("std::fs::File::write_all", "Fs"),
7572 ("std::net::TcpStream::write_all", "Net"),
7573 ("std::os::unix::net::UnixStream::send", "Ipc"),
7574 // the TERMINAL VERBS of the two option-builders — the whole point of routing them.
7575 ("std::fs::OpenOptions::open", "Fs"),
7576 ("std::fs::DirBuilder::create", "Fs"),
7577 ] {
7578 assert_eq!(classify("std", p), Some(want), "`{p}` must stay {want}");
7579 }
7580 }
7581
7582 #[test]
7583 fn db_crates_are_calibrated() {
7584 // The calibrated set must cover every DB client the classifier knows, or the receipt's coverage
7585 // check would flag a recognized crate as a blind spot. (Was nightly-lint-only; now runs on stable.)
7586 for c in DB_CRATES {
7587 assert!(
7588 CALIBRATED_CRATES.contains(&c),
7589 "DB crate `{c}` is matched by classify() but missing from CALIBRATED_CRATES"
7590 );
7591 }
7592 }
7593
7594 /// The two coverage lists mean OPPOSITE things and must stay disjoint.
7595 ///
7596 /// `CALIBRATED_CRATES` = "classify has effect rules here". `REVIEWED_PURE_CRATES` = "read it, it
7597 /// performs nothing". A crate in both would be asserting both at once, and the ledger consults them
7598 /// with an OR — so the contradiction would resolve silently to "covered" and nobody would look again.
7599 #[test]
7600 fn reviewed_pure_and_calibrated_are_disjoint() {
7601 for c in REVIEWED_PURE_CRATES {
7602 assert!(!CALIBRATED_CRATES.contains(&c),
7603 "`{c}` is in BOTH lists — it cannot be rule-covered AND effect-free");
7604 assert!(!PATH_CALIBRATED_CRATES.contains(&c), "`{c}` is in BOTH lists (path-calibrated)");
7605 assert!(!CALIBRATED_PREFIXES.iter().any(|p| c.starts_with(p)),
7606 "`{c}` is covered by a calibrated PREFIX as well as the pure list");
7607 }
7608 }
7609
7610 /// A reviewed-pure crate must actually classify as pure — the mirror of `calibrated_crates_are_live`.
7611 ///
7612 /// The list makes candor BELIEVE these crates rather than disclose them, so if someone later adds a
7613 /// rule for one, the claim "performs no effect of its own" is dead and the entry has to be re-read,
7614 /// not silently outvoted by the rule. Probed with the same tails the liveness test uses, which is a
7615 /// broad sweep of the effectful verb shapes candor knows.
7616 #[test]
7617 fn reviewed_pure_crates_classify_as_pure() {
7618 for c in REVIEWED_PURE_CRATES {
7619 for t in CALIBRATION_PROBE_TAILS {
7620 assert!(classify(c, &format!("{c}{t}")).is_none(),
7621 "`{c}` is listed REVIEWED-PURE but classify() gives it an effect on `{c}{t}` — \
7622 one of the two is wrong, and the list is the claim");
7623 }
7624 }
7625 }
7626
7627 /// ⟨CALIBRATION CEILING — the table must agree with the REVIEW RECORD⟩ SOUNDNESS R328.
7628 ///
7629 /// `CALIBRATED_CEILINGS` is the version each crate's rules were actually written against. Nothing
7630 /// derived it from, or checked it against, the review record — and three separate failures followed
7631 /// in one day: a wholesale table rewrite silently DROPPED nine entries while the commit message
7632 /// claimed four were absent "each with a stated reason"; `git2` was ceilinged ABOVE the only version
7633 /// its rules record reading (the SILENT direction, and 0.21 added `author_from_env` /
7634 /// `committer_from_env` / `Oid::hash_file_ext`, none of them ruled); and `sqlx_core` sat BELOW the
7635 /// version its own comment says was verified, which is also an impossible sibling of `sqlx 0.9.0`'s
7636 /// `=0.9.0` pin. All three were invisible to 410 tests and 29 gates.
7637 ///
7638 /// This asserts the one invariant that catches all of them: **a crate whose rules cite a reviewed
7639 /// version must carry a ceiling, and it must be THAT version.** Citations are read with wrapped
7640 /// comment lines joined and both name spellings tried — R315, where a line-by-line dashed-only
7641 /// sweep silently under-counted the citations by three.
7642 #[test]
7643 fn calibration_ceilings_match_the_recorded_review() {
7644 let src = include_str!("lib.rs");
7645 // join wrapped comment continuations, then collapse whitespace: a citation is prose and prose wraps.
7646 let flat = src
7647 .replace("\n //", " ")
7648 .replace("\n //", " ")
7649 .replace("\n //", " ");
7650 let flat: String = flat.split_whitespace().collect::<Vec<_>>().join(" ");
7651 let ceilings: std::collections::HashMap<&str, &str> =
7652 CALIBRATED_CEILINGS.iter().copied().collect();
7653 // Absences are deliberate and enumerated in the table's own comment; keep the list HERE too so
7654 // dropping one silently is impossible — that is exactly what happened.
7655 let deliberate = ["postgres", "async_fs", "getrandom", "rand_core"];
7656 let mut problems: Vec<String> = Vec::new();
7657 for c in CALIBRATED_CRATES {
7658 if deliberate.contains(&c) {
7659 continue;
7660 }
7661 // EVERY citation, both verbs, both name spellings, and slash-separated lists —
7662 // "verified against sqlx-core 0.8.6/0.9.0" records TWO reviewed versions, and a crate may
7663 // be cited in more than one comment ("validated against rusqlite 0.39" elsewhere). An
7664 // extractor that stops at the first hit under-collects, and under-collecting here makes a
7665 // correct ceiling look like it is above the record — R315's lesson, one level up.
7666 let mut cited: Vec<String> = Vec::new();
7667 for form in [c.to_string(), c.replace('_', "-")] {
7668 for verb in ["verified against ", "validated against "] {
7669 let pat = format!("{verb}{form} ");
7670 let mut from = 0usize;
7671 while let Some(i) = flat[from..].find(&pat) {
7672 let at = from + i + pat.len();
7673 let tail: String = flat[at..]
7674 .chars()
7675 .take_while(|ch| ch.is_ascii_digit() || *ch == '.' || *ch == '/')
7676 .collect();
7677 for v in tail.split('/') {
7678 let v = v.trim_end_matches('.').to_string();
7679 if v.split('.').count() >= 2 && !cited.contains(&v) {
7680 cited.push(v);
7681 }
7682 }
7683 from = at;
7684 }
7685 }
7686 }
7687 if cited.is_empty() {
7688 continue;
7689 }
7690 match ceilings.get(c) {
7691 None => problems.push(format!(
7692 "{c}: rules cite {} but there is NO ceiling — the exemption is unconditional",
7693 cited.join("/")
7694 )),
7695 // A comment may legitimately record MORE THAN ONE reviewed version
7696 // ("verified against sqlx-core 0.8.6/0.9.0"), so matching any of them is fine, and a
7697 // ceiling BELOW the newest review is merely noisy. The failure that matters is a
7698 // ceiling ABOVE EVERY recorded review: that grants reviewed-purity over source nobody
7699 // read, which is the SILENT direction and the whole reason this table exists.
7700 Some(have) => {
7701 let ok = cited.iter().any(|v| have.starts_with(v) || v.starts_with(*have))
7702 || cited.iter().any(|v| calibration_exceeded_raw(v, have));
7703 if !ok {
7704 problems.push(format!(
7705 "{c}: ceiling {have} is ABOVE every recorded review ({}) — it grants \
7706 reviewed-purity over source no comment records anyone reading",
7707 cited.join("/")
7708 ));
7709 }
7710 }
7711 }
7712 }
7713 assert!(
7714 problems.is_empty(),
7715 "CALIBRATED_CEILINGS disagrees with the review record:\n {}\n\n\
7716 A ceiling ABOVE the recorded review grants reviewed-purity over source nobody read (SILENT); \
7717 a ceiling BELOW it is noisy but safe. Either way the table and the record must agree, or \
7718 neither is evidence.",
7719 problems.join("\n ")
7720 );
7721 }
7722
7723 #[test]
7724 fn calibrated_crates_are_live() {
7725 // Conversely, every crate advertised as calibrated must actually be matched by classify() for
7726 // some representative path — a dead entry would silently suppress a real coverage warning.
7727 for c in CALIBRATED_CRATES {
7728 assert!(
7729 CALIBRATION_PROBE_TAILS.iter().any(|t| classify(c, &format!("{c}{t}")).is_some()),
7730 "calibrated crate `{c}` is matched by no path in classify() — dead list entry"
7731 );
7732 }
7733 }
7734
7735 #[test]
7736 fn async_http_stack_classifies() {
7737 // The modern async-HTTP/TLS/QUIC/DNS stack (found by the independent-method differential on oha):
7738 // verb-keyed Net/Ipc/Fs/Env, crate-gated so generic verbs never fabricate across crates.
7739 assert_eq!(classify("hyper", "hyper::client::conn::http1::SendRequest::send_request"), Some("Net"));
7740 assert_eq!(classify("hyper", "hyper::client::conn::http1::handshake"), Some("Net"));
7741 assert_eq!(classify("hyper_util", "hyper_util::client::legacy::Client::request"), Some("Net"));
7742 assert_eq!(classify("hickory_resolver", "hickory_resolver::Resolver::lookup_ip"), Some("Net"));
7743 assert_eq!(classify("quinn", "quinn::Endpoint::connect"), Some("Net"));
7744 assert_eq!(classify("quinn", "quinn::RecvStream::read_to_end"), Some("Net")); // stream byte I/O, not just open
7745 assert_eq!(classify("quinn", "quinn::SendStream::write_all"), Some("Net"));
7746 assert_eq!(classify("tokio_rustls", "tokio_rustls::TlsConnector::connect"), Some("Net"));
7747 assert_eq!(classify("native_tls", "native_tls::TlsConnector::connect"), Some("Net"));
7748 assert_eq!(classify("tokio_vsock", "tokio_vsock::VsockStream::connect"), Some("Ipc"));
7749 assert_eq!(classify("rustls_native_certs", "rustls_native_certs::load_native_certs"), Some("Fs"));
7750 assert_eq!(classify("rlimit", "rlimit::setrlimit"), Some("Env"));
7751 // num_cpus is deliberately PURE (consistency with std::thread::available_parallelism; avoids Env spray)
7752 assert_eq!(classify("num_cpus", "num_cpus::get"), None);
7753 assert_eq!(classify("num_cpus", "num_cpus::get_physical"), None);
7754 // pure surface stays None (no fabrication): builder/type/config paths, and other crates' generic verbs
7755 assert_eq!(classify("hyper", "hyper::Request::builder"), None);
7756 assert_eq!(classify("hyper", "hyper::body::Bytes::new"), None);
7757 assert_eq!(classify("native_tls", "native_tls::TlsConnectorBuilder::min_protocol_version"), None);
7758 assert_eq!(classify("serde", "serde::Deserialize::request"), None); // generic verb, wrong crate
7759 }
7760
7761 #[test]
7762 fn coverage_differential_crates_classify() {
7763 // Crates the coverage differential found DISCLOSED-but-unmodeled. Each rule is verb-keyed +
7764 // crate-gated; the EFFECT verbs map to the right bucket and the PURE surface stays None (a
7765 // wrongly-flagged pure crate is a fabrication, so the negatives matter as much as the positives).
7766
7767 // rustls (sync TLS core) — record I/O is Net; config/cert + the buffered-decrypt step are pure.
7768 assert_eq!(classify("rustls", "rustls::ClientConnection::read_tls"), Some("Net"));
7769 assert_eq!(classify("rustls", "rustls::ConnectionCommon::write_tls"), Some("Net"));
7770 assert_eq!(classify("rustls", "rustls::Connection::complete_io"), Some("Net"));
7771 assert_eq!(classify("rustls", "rustls::ConnectionCommon::process_new_packets"), None); // buffered decrypt, no I/O
7772 assert_eq!(classify("rustls", "rustls::ClientConfig::builder"), None); // pure config
7773
7774 // native-tls variants — handshake is Net; builder is pure.
7775 assert_eq!(classify("native_tls_crate", "native_tls_crate::TlsConnector::connect"), Some("Net"));
7776 assert_eq!(classify("tokio_native_tls", "tokio_native_tls::TlsAcceptor::accept"), Some("Net"));
7777 assert_eq!(classify("native_tls_crate", "native_tls_crate::TlsConnectorBuilder::min_protocol_version"), None);
7778
7779 // etcetera — dir resolution reads env; the args data type is pure.
7780 assert_eq!(classify("etcetera", "etcetera::home_dir"), Some("Env"));
7781 assert_eq!(classify("etcetera", "etcetera::base_strategy::choose_base_strategy"), Some("Env"));
7782 assert_eq!(classify("etcetera", "etcetera::base_strategy::Xdg::config_dir"), Some("Env"));
7783 assert_eq!(classify("etcetera", "etcetera::app_strategy::AppStrategyArgs::new"), None); // pure data
7784
7785 // sqlx-core — connect is Net, execute/fetch round-trips are Db; options/builders pure.
7786 assert_eq!(classify("sqlx_core", "sqlx_core::connection::Connection::connect"), Some("Net"));
7787 assert_eq!(classify("sqlx_core", "sqlx_core::executor::Executor::fetch_one"), Some("Db"));
7788 assert_eq!(classify("sqlx_core", "sqlx_core::executor::Executor::execute"), Some("Db"));
7789 assert_eq!(classify("sqlx_core", "sqlx_core::pool::Pool::acquire"), Some("Db"));
7790 assert_eq!(classify("sqlx_core", "sqlx_core::pool::PoolOptions::max_connections"), None); // pure builder
7791
7792 // walkdir — charged at construction (`WalkDir::new`, mirroring `ignore`/`glob`), NOT solely at
7793 // the lazy `next()`/`metadata()` read: candor-scan's receiver-typing blocklist hard-blocks
7794 // `.into_iter()`, so a typed `IntoIter::next` receiver is unreachable from the idiomatic
7795 // `WalkDir::new(p).into_iter()...` chain — `WalkDir::new` is the only point every real usage
7796 // (for-loop, `.count()`, untyped `.next()`) actually reaches. `IntoIter::next`/`DirEntry::metadata`
7797 // stay Some("Fs") as a secondary rule for the narrower explicit-type-annotation case. Builder
7798 // setters (`into_iter` itself, `max_depth`, …) and cached accessors stay pure.
7799 assert_eq!(classify("walkdir", "walkdir::WalkDir::new"), Some("Fs"));
7800 assert_eq!(classify("walkdir", "walkdir::IntoIter::next"), Some("Fs"));
7801 assert_eq!(classify("walkdir", "walkdir::DirEntry::metadata"), Some("Fs"));
7802 assert_eq!(classify("walkdir", "walkdir::WalkDir::into_iter"), None); // no I/O until pulled
7803 assert_eq!(classify("walkdir", "walkdir::WalkDir::max_depth"), None); // builder setter
7804 assert_eq!(classify("walkdir", "walkdir::DirEntry::file_type"), None); // cached, no syscall
7805
7806 // filetime — set_* are utimes (Fs), now is Clock; from_* constructors pure.
7807 assert_eq!(classify("filetime", "filetime::set_file_mtime"), Some("Fs"));
7808 assert_eq!(classify("filetime", "filetime::set_file_handle_times"), Some("Fs"));
7809 assert_eq!(classify("filetime", "filetime::FileTime::now"), Some("Clock"));
7810 assert_eq!(classify("filetime", "filetime::FileTime::from_unix_time"), None);
7811 assert_eq!(classify("filetime", "filetime::FileTime::from_last_modification_time"), None); // reads &Metadata, not disk
7812
7813 // execute — the execute* verbs spawn (Exec); command/shell builders pure.
7814 assert_eq!(classify("execute", "execute::Execute::execute"), Some("Exec"));
7815 assert_eq!(classify("execute", "execute::Execute::execute_output"), Some("Exec"));
7816 assert_eq!(classify("execute", "execute::Execute::execute_multiple_output"), Some("Exec"));
7817 assert_eq!(classify("execute", "execute::command"), None); // only builds a Command
7818 assert_eq!(classify("execute", "execute::shell"), None);
7819
7820 // ctrlc — install signal handler (Ipc).
7821 assert_eq!(classify("ctrlc", "ctrlc::set_handler"), Some("Ipc"));
7822 assert_eq!(classify("ctrlc", "ctrlc::try_set_handler"), Some("Ipc"));
7823
7824 // clap — only the argv-reading terminals are Env; the whole builder + *_from variants pure.
7825 assert_eq!(classify("clap", "clap::Command::get_matches"), Some("Env"));
7826 assert_eq!(classify("clap", "clap::Command::try_get_matches"), Some("Env"));
7827 assert_eq!(classify("clap", "clap::Parser::parse"), Some("Env"));
7828 assert_eq!(classify("clap", "clap::Command::new"), None); // builder
7829 assert_eq!(classify("clap", "clap::Arg::about"), None); // builder
7830 assert_eq!(classify("clap", "clap::Command::get_matches_from"), None); // explicit args, no argv read
7831 // R59-class audit: `Arg::env` calls `env::var_os` DIRECTLY at builder time (verified against
7832 // clap_builder 4.6.6) — unambiguous (the crate's only `::env` method), so classify it rather
7833 // than leave a real effect to fall silently into the CALIBRATED_CRATES purity exemption.
7834 assert_eq!(classify("clap", "clap::Arg::env"), Some("Env"));
7835 assert_eq!(classify("clap", "clap::Arg::env_os"), Some("Env"));
7836
7837 // jiff — now* is Clock; tz lookups read the tzdb (Fs); span/civil math pure.
7838 assert_eq!(classify("jiff", "jiff::Timestamp::now"), Some("Clock"));
7839 assert_eq!(classify("jiff", "jiff::Zoned::now_with"), Some("Clock"));
7840 assert_eq!(classify("jiff", "jiff::tz::TimeZone::system"), Some("Fs"));
7841 assert_eq!(classify("jiff", "jiff::tz::TimeZone::get"), Some("Fs"));
7842 assert_eq!(classify("jiff", "jiff::Span::checked_add"), None); // pure arithmetic
7843
7844 // env_logger — init installs the logger + reads RUST_LOG (Log); config setters pure.
7845 // TUI — the tty is a user dialogue channel (Ipc), the ruling dialoguer/console already carry.
7846 // Each verb below was read off the crate source (crossterm-0.28.1, ratatui-0.29.0), not guessed.
7847 assert_eq!(classify("crossterm", "crossterm::ExecutableCommand::execute"), Some("Ipc"));
7848 assert_eq!(classify("crossterm", "crossterm::QueueableCommand::queue"), Some("Ipc"));
7849 assert_eq!(classify("crossterm", "crossterm::event::read"), Some("Ipc"));
7850 assert_eq!(classify("crossterm", "crossterm::event::poll"), Some("Ipc"));
7851 assert_eq!(classify("crossterm", "crossterm::terminal::enable_raw_mode"), Some("Ipc"));
7852 // a tty IOCTL must not fall through: in a CALIBRATED crate an unmatched path is a purity CLAIM
7853 assert_eq!(classify("crossterm", "crossterm::terminal::size"), Some("Ipc"));
7854 // the Command VALUE types are pure — they describe an action, they do not perform one
7855 assert_eq!(classify("crossterm", "crossterm::style::Print"), None);
7856 assert_eq!(classify("crossterm", "crossterm::cursor::MoveTo"), None);
7857
7858 // ratatui: the backlog said "mark reviewed-pure"; the SOURCE says `Terminal::draw` ends in a
7859 // backend flush, so the write surface is Ipc and only the render surface is pure.
7860 assert_eq!(classify("ratatui", "ratatui::Terminal::draw"), Some("Ipc"));
7861 assert_eq!(classify("ratatui", "ratatui::Terminal::flush"), Some("Ipc"));
7862 assert_eq!(classify("ratatui", "ratatui::Terminal::clear"), Some("Ipc"));
7863 assert_eq!(classify("ratatui", "ratatui::Terminal::hide_cursor"), Some("Ipc"));
7864 // REGRESSION: `widgets::canvas` is an in-memory grid. `Context::draw` ends in `::draw` and was
7865 // FABRICATING Ipc — caught in review, measured on a fixture, and a hot path (per shape, per frame).
7866 assert_eq!(classify("ratatui", "ratatui::widgets::canvas::Context::draw"), None);
7867 assert_eq!(classify("ratatui", "ratatui::widgets::canvas::Context::layer"), None);
7868 // …while the real write surface still classifies, including a DIRECT backend call (which is why
7869 // the carve-out is a denylist on canvas rather than an allowlist on `Terminal::`).
7870 assert_eq!(classify("ratatui", "ratatui::backend::CrosstermBackend::flush"), Some("Ipc"));
7871 // the BULK of the 3,345 disclosed calls — widgets, layout, style — are genuinely pure
7872 assert_eq!(classify("ratatui", "ratatui::widgets::Paragraph::new"), None);
7873 assert_eq!(classify("ratatui", "ratatui::layout::Layout::split"), None);
7874 assert_eq!(classify("ratatui", "ratatui::style::Style::fg"), None);
7875 assert_eq!(classify("ratatui", "ratatui::buffer::Buffer::set_string"), None);
7876
7877 // tracing_subscriber — two effects, both read off 0.3.23. The filing said "Log/Fs"; the Fs half
7878 // is wrong (the crate ACCEPTS a File as a writer, it never opens one).
7879 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::init"), Some("Log"));
7880 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::try_init"), Some("Log"));
7881 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::SubscriberBuilder::init"), Some("Log"));
7882 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::EnvFilter::from_default_env"), Some("Env"));
7883 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::EnvFilter::from_env"), Some("Env"));
7884 // the setters DESCRIBE a subscriber; they do not install one. (`fmt::layer`/`fmt::fmt` are NOT setters:
7885 // they construct through `Layer::default`, which reads NO_COLOR — R1528, `r1528_clock_env_rows_classify`.)
7886 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::layer"), Some("Env"));
7887 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::SubscriberBuilder::with_target"), None);
7888 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::EnvFilter::new"), None);
7889
7890 assert_eq!(classify("env_logger", "env_logger::init"), Some("Log"));
7891 assert_eq!(classify("env_logger", "env_logger::try_init"), Some("Log"));
7892 assert_eq!(classify("env_logger", "env_logger::Builder::init"), Some("Log"));
7893 assert_eq!(classify("env_logger", "env_logger::Builder::format_timestamp"), None); // config
7894 assert_eq!(classify("env_logger", "env_logger::Builder::build"), None); // pure build
7895
7896 // dialoguer — interact* is tty I/O (Ipc); builders pure.
7897 assert_eq!(classify("dialoguer", "dialoguer::Input::interact_text"), Some("Ipc"));
7898 assert_eq!(classify("dialoguer", "dialoguer::Confirm::interact"), Some("Ipc"));
7899 assert_eq!(classify("dialoguer", "dialoguer::Select::interact_opt"), Some("Ipc"));
7900 assert_eq!(classify("dialoguer", "dialoguer::Input::with_prompt"), None); // builder
7901 // widened-CORE audit: `Editor::new`/`::default` read VISUAL/EDITOR at construction, independent
7902 // of `Editor::edit`'s already-covered Exec (spawning that editor is a separate, later step).
7903 assert_eq!(classify("dialoguer", "dialoguer::Editor::new"), Some("Env"));
7904 assert_eq!(classify("dialoguer", "dialoguer::Editor::default"), Some("Env"));
7905 assert_eq!(classify("dialoguer", "dialoguer::Editor::edit"), Some("Exec"));
7906 assert_eq!(classify("dialoguer", "dialoguer::Editor::extension"), None); // pure setter
7907
7908 // console — Term I/O is Ipc, detection is Env, Style is pure.
7909 assert_eq!(classify("console", "console::Term::write_line"), Some("Ipc"));
7910 assert_eq!(classify("console", "console::Term::read_key"), Some("Ipc"));
7911 assert_eq!(classify("console", "console::colors_enabled"), Some("Env"));
7912 assert_eq!(classify("console", "console::Style::cyan"), None); // pure styling
7913 assert_eq!(classify("console", "console::strip_ansi_codes"), None); // pure text util
7914 // R59-class audit: `Term` also implements raw `io::{Read,Write}` (console 0.15.11) — the same
7915 // tty channel as `write_line`/`read_key`, reached through the generic trait methods instead of
7916 // the crate's own named convenience methods. Crate-gated and `Term::`-scoped, so it cannot spread.
7917 assert_eq!(classify("console", "console::Term::write"), Some("Ipc"));
7918 assert_eq!(classify("console", "console::Term::flush"), Some("Ipc"));
7919 assert_eq!(classify("console", "console::Term::read"), Some("Ipc"));
7920
7921 // terminal_colorsaurus — tty colour query (Ipc).
7922 assert_eq!(classify("terminal_colorsaurus", "terminal_colorsaurus::background_color"), Some("Ipc"));
7923 assert_eq!(classify("terminal_colorsaurus", "terminal_colorsaurus::color_palette"), Some("Ipc"));
7924
7925 // backoff — retry sleeps + reads the clock (Clock); the setters are pure, but the CONSTRUCTORS are not: `default()` and
7926 // `ExponentialBackoffBuilder::build` stamp `start_time: Instant::now()` (EXECUTED 2 / 1 clock reads, R1528 b11).
7927 assert_eq!(classify("backoff", "backoff::retry"), Some("Clock"));
7928 assert_eq!(classify("backoff", "backoff::retry_notify"), Some("Clock"));
7929 assert_eq!(classify("backoff", "backoff::ExponentialBackoff::default"), Some("Clock"));
7930
7931 // lscolors — ONLY from_env reads the environment; from_string/style_for_path pure.
7932 assert_eq!(classify("lscolors", "lscolors::LsColors::from_env"), Some("Env"));
7933 assert_eq!(classify("lscolors", "lscolors::LsColors::from_string"), None);
7934 assert_eq!(classify("lscolors", "lscolors::LsColors::style_for_path"), None);
7935
7936 // wild — argv readers (Env).
7937 assert_eq!(classify("wild", "wild::args"), Some("Env"));
7938 assert_eq!(classify("wild", "wild::args_os"), Some("Env"));
7939
7940 // grep_cli — only the firm Exec (CommandReader spawn); the isatty probes stay unmodeled.
7941 assert_eq!(classify("grep_cli", "grep_cli::CommandReaderBuilder::build"), Some("Exec"));
7942 assert_eq!(classify("grep_cli", "grep_cli::is_readable_stdin"), None); // isatty/fstat, not modeled
7943 assert_eq!(classify("grep_cli", "grep_cli::is_tty_stdout"), None);
7944
7945 // clircle — same-file detection issues fstat/lseek (Fs); equality is pure.
7946 assert_eq!(classify("clircle", "clircle::Identifier::try_from"), Some("Fs"));
7947 assert_eq!(classify("clircle", "clircle::Clircle::surely_conflicts_with"), Some("Fs"));
7948 }
7949
7950 #[test]
7951 fn log_tracing_emit_macros_classify_pre_expansion() {
7952 // candor-scan is pre-expansion: it sees the raw macro path (`log::info`, `tracing::warn`), not the
7953 // expanded dispatch the deep engine sees. Both the user-facing macro names AND the type surface:
7954 assert_eq!(classify("log", "log::info"), Some("Log"));
7955 assert_eq!(classify("log", "log::error"), Some("Log"));
7956 assert_eq!(classify("tracing", "tracing::warn"), Some("Log"));
7957 assert_eq!(classify("tracing", "tracing::info_span"), Some("Log"));
7958 // pure data-type surface stays None (no fabricated Log)
7959 assert_eq!(classify("log", "log::Level::as_str"), None);
7960 assert_eq!(classify("tracing", "tracing::Level::INFO"), None);
7961 }
7962
7963 #[test]
7964 fn classify_core_effects() {
7965 // A representative smoke test of the classifier's main families, so the published crate is not
7966 // shipped untested (these used to live only in the nightly-only src/lib.rs).
7967 assert_eq!(classify("std", "std::fs::read_to_string"), Some("Fs"));
7968 // std::path stat-family methods are Fs (each is a stat/readdir syscall); the pure
7969 // string-manipulation surface stays unclassified (the blackout screen's gix-dir find).
7970 assert_eq!(classify("std", "std::path::Path::symlink_metadata"), Some("Fs"));
7971 assert_eq!(classify("std", "std::path::PathBuf::read_dir"), Some("Fs"));
7972 assert_eq!(classify("std", "std::path::Path::exists"), Some("Fs"));
7973 assert_eq!(classify("std", "std::path::Path::join"), None); // pure string manipulation
7974 assert_eq!(classify("std", "std::path::PathBuf::file_name"), None);
7975 assert_eq!(classify("std", "std::path::Path::parent"), None);
7976 assert_eq!(classify("std", "std::process::Command::new"), Some("Exec"));
7977 assert_eq!(classify("std", "std::env::var"), Some("Env"));
7978 assert_eq!(classify("reqwest", "reqwest::Client::execute"), Some("Net"));
7979 // one-shot convenience fns send immediately → Net.
7980 assert_eq!(classify("reqwest", "reqwest::get"), Some("Net"));
7981 assert_eq!(classify("reqwest", "reqwest::blocking::get"), Some("Net"));
7982 // the URL-BEARING builder methods classify Net too — the DOMINANT idiom is the builder chain
7983 // `Client::new().post(url).send()`, whose URL literal rides the `.post(url)` step (NOT `.send()`),
7984 // so the endpoint (and the Llm host refinement) only get captured if the URL-naming step is Net.
7985 assert_eq!(classify("reqwest", "reqwest::Client::get"), Some("Net"));
7986 assert_eq!(classify("reqwest", "reqwest::Client::post"), Some("Net"));
7987 assert_eq!(classify("reqwest", "reqwest::Client::put"), Some("Net"));
7988 assert_eq!(classify("reqwest", "reqwest::Client::delete"), Some("Net"));
7989 assert_eq!(classify("reqwest", "reqwest::Client::request"), Some("Net"));
7990 // the PURE builder surface stays None (no URL, no dispatch).
7991 assert_eq!(classify("reqwest", "reqwest::RequestBuilder::header"), None);
7992 assert_eq!(classify("reqwest", "reqwest::RequestBuilder::json"), None);
7993 // R1528 (b10): `build` dispatches nothing (still not `Net`), but it reads the proxy variables on every call (EXECUTED:
7994 // `ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and lowercase), so it is `Env`, not "pure".
7995 assert_eq!(classify("reqwest", "reqwest::ClientBuilder::build"), Some("Env"));
7996 // RAW POSIX SOCKETS — the lowest network tier, pinned as a regression guard (four-way close:
7997 // swift got a raw-socket regression this week from a bare-identifier collision; rust never had
7998 // the gap because it classifies path-QUALIFIED via the syscall-leaf table, but pin it so the
7999 // `socket`/`connect` Net rows can't silently drop). `libc::connect`/`libc::socket` are the direct
8000 // FFI syscalls; `nix::sys::socket::connect` is the safe wrapper; both bottom out in the NET table.
8001 assert_eq!(classify("libc", "libc::connect"), Some("Net"));
8002 assert_eq!(classify("libc", "libc::socket"), Some("Net"));
8003 assert_eq!(classify("libc", "libc::bind"), Some("Net"));
8004 assert_eq!(classify("libc", "libc::accept"), Some("Net"));
8005 // nix routes through the libc syscall table (same leaves): I/O classified, generic fd ops skipped.
8006 assert_eq!(classify("nix", "nix::fcntl::open"), Some("Fs"));
8007 assert_eq!(classify("nix", "nix::sys::socket::connect"), Some("Net"));
8008 assert_eq!(classify("nix", "nix::sys::socket::socket"), Some("Net"));
8009 assert_eq!(classify("nix", "nix::unistd::execvp"), Some("Exec"));
8010 assert_eq!(classify("nix", "nix::unistd::write"), None); // generic fd op — deliberately unclassified
8011 assert_eq!(classify("nix", "nix::unistd::getpid"), None); // not I/O
8012 // rustix does raw syscalls (no libc underneath) → classified directly by leaf, same table.
8013 assert_eq!(classify("rustix", "rustix::time::clock_settime"), Some("Clock"));
8014 assert_eq!(classify("rustix", "rustix::fs::symlink"), Some("Fs"));
8015 assert_eq!(classify("rustix", "rustix::net::connect"), Some("Net"));
8016 assert_eq!(classify("rustix", "rustix::io::read"), None); // generic fd op
8017 // pnet raw packet capture: channel openers are Net, packet construction stays pure.
8018 assert_eq!(classify("pnet", "pnet::datalink::channel"), Some("Net"));
8019 assert_eq!(classify("pnet", "pnet::transport::transport_channel"), Some("Net"));
8020 assert_eq!(classify("pnet_datalink", "pnet_datalink::channel"), Some("Net"));
8021 assert_eq!(classify("pnet", "pnet::packet::ethernet::EthernetPacket::new"), None);
8022 assert_eq!(classify("pnet_base", "pnet_base::MacAddr::new"), None);
8023 // ignore (gitignore-aware walker): walk executors are Fs, config builders stay pure.
8024 assert_eq!(classify("ignore", "ignore::WalkBuilder::build_parallel"), Some("Fs"));
8025 assert_eq!(classify("ignore", "ignore::WalkBuilder::build"), Some("Fs"));
8026 assert_eq!(classify("ignore", "ignore::WalkParallel::run"), Some("Fs"));
8027 assert_eq!(classify("ignore", "ignore::WalkBuilder::add_ignore"), Some("Fs")); // reads the ignore file
8028 assert_eq!(classify("ignore", "ignore::overrides::OverrideBuilder::build"), None); // pure config
8029 assert_eq!(classify("ignore", "ignore::gitignore::GitignoreBuilder::build"), None); // pure config
8030 assert_eq!(classify("ignore", "ignore::DirEntry::path"), None); // pure accessor
8031 // THE FIX: `Walk::new`/`Walk::from_iter` are `WalkBuilder::new(path).build()` /
8032 // `WalkBuilder::from_iter(paths).build()` in ignore's own source — the crate's documented
8033 // convenience constructors, and its own top-level doc example (`for entry in Walk::new(path)`).
8034 // A corpus round caught `deny Fs` exiting 0 over exactly that idiom.
8035 assert_eq!(classify("ignore", "ignore::Walk::new"), Some("Fs"));
8036 assert_eq!(classify("ignore", "ignore::Walk::from_iter"), Some("Fs"));
8037 // NO FABRICATION: a same-named `Walk::new` from an unrelated crate must not gain Fs — crate-gated.
8038 assert_eq!(classify("walkdir", "walkdir::Walk::new"), None);
8039 assert_eq!(classify("some_local_crate", "some_local_crate::Walk::new"), None);
8040 // notify fs-watching: watcher constructors + watch/unwatch are Fs, data types stay pure.
8041 assert_eq!(classify("notify", "notify::RecommendedWatcher::new"), Some("Fs"));
8042 assert_eq!(classify("notify", "notify::PollWatcher::new"), Some("Fs"));
8043 assert_eq!(classify("notify", "notify::recommended_watcher"), Some("Fs"));
8044 assert_eq!(classify("notify", "notify::INotifyWatcher::watch"), Some("Fs"));
8045 assert_eq!(classify("notify", "notify::Config::default"), None); // pure config
8046 assert_eq!(classify("notify", "notify::Event::new"), None); // pure data type
8047 assert_eq!(classify("rusqlite", "rusqlite::Connection::execute"), Some("Db"));
8048 // the rusqlite verb DIALECT (a verb probe found the canonical consumer API classifying pure):
8049 assert_eq!(classify("rusqlite", "rusqlite::Connection::query_row"), Some("Db"));
8050 assert_eq!(classify("rusqlite", "rusqlite::Statement::query_map"), Some("Db"));
8051 assert_eq!(classify("rusqlite", "rusqlite::Connection::execute_batch"), Some("Db"));
8052 assert_eq!(classify("rusqlite", "rusqlite::Connection::prepare_cached"), Some("Db"));
8053 assert_eq!(classify("rusqlite", "rusqlite::Connection::open"), Some("Db"));
8054 assert_eq!(classify("rusqlite", "rusqlite::Connection::open_in_memory"), Some("Db"));
8055 // THE FIX: three more real `Connection::open*` constructors an exact-suffix list missed
8056 // (`open_in_memory_with_flags` doesn't END in `open_with_flags`), plus the documented blob-I/O
8057 // API (`blob_open`/`Blob::reopen`), each calling the real sqlite3 FFI directly.
8058 assert_eq!(classify("rusqlite", "rusqlite::Connection::open_in_memory_with_flags"), Some("Db"));
8059 assert_eq!(classify("rusqlite", "rusqlite::Connection::open_with_flags_and_vfs"), Some("Db"));
8060 assert_eq!(
8061 classify("rusqlite", "rusqlite::Connection::open_in_memory_with_flags_and_vfs"),
8062 Some("Db")
8063 );
8064 assert_eq!(classify("rusqlite", "rusqlite::Connection::blob_open"), Some("Db"));
8065 assert_eq!(classify("rusqlite", "rusqlite::blob::Blob::reopen"), Some("Db"));
8066 // NO FABRICATION: a same-crate, DIFFERENT type's `open`-prefixed method (rusqlite's own private
8067 // `pragma::Sql::open_brace`, which pushes one char to a string buffer) must not gain Db — the
8068 // fix keys on the `Connection::` segment, not a bare leaf prefix.
8069 assert_eq!(classify("rusqlite", "rusqlite::pragma::Sql::open_brace"), None);
8070 // THE COVERAGE-GATE SWEEP FIX: the online-backup API and incremental-BLOB positional I/O,
8071 // verified against rusqlite 0.32.1 (each calls an `ffi::sqlite3_*` leaf already in this file's
8072 // own FFI-leaf DB table, but the `crate_name == "rusqlite"` block above returned before ever
8073 // reaching it — same shape as the git2 fix elsewhere in this file).
8074 assert_eq!(classify("rusqlite", "rusqlite::Backup::new"), Some("Db"));
8075 assert_eq!(classify("rusqlite", "rusqlite::Backup::new_with_names"), Some("Db"));
8076 assert_eq!(classify("rusqlite", "rusqlite::Backup::step"), Some("Db"));
8077 assert_eq!(classify("rusqlite", "rusqlite::Backup::run_to_completion"), Some("Db"));
8078 assert_eq!(classify("rusqlite", "rusqlite::Connection::backup"), Some("Db"));
8079 assert_eq!(classify("rusqlite", "rusqlite::Connection::restore"), Some("Db"));
8080 assert_eq!(classify("rusqlite", "rusqlite::Blob::read_at"), Some("Db"));
8081 assert_eq!(classify("rusqlite", "rusqlite::Blob::read_at_exact"), Some("Db"));
8082 assert_eq!(classify("rusqlite", "rusqlite::Blob::raw_read_at"), Some("Db"));
8083 assert_eq!(classify("rusqlite", "rusqlite::Blob::raw_read_at_exact"), Some("Db"));
8084 assert_eq!(classify("rusqlite", "rusqlite::Blob::write_at"), Some("Db"));
8085 assert_eq!(classify("rusqlite", "rusqlite::Blob::write_all_at"), Some("Db"));
8086 assert_eq!(classify("rusqlite", "rusqlite::Connection::from_handle"), Some("Db"));
8087 assert_eq!(classify("rusqlite", "rusqlite::Connection::from_handle_owned"), Some("Db"));
8088 assert_eq!(classify("rusqlite", "rusqlite::Connection::extension_init2"), Some("Db"));
8089 assert_eq!(classify("rusqlite", "rusqlite::init_auto_extension"), Some("Db"));
8090 // …and the REAL module-qualified spellings (`Backup`/`Blob`/`init_auto_extension` are not
8091 // re-exported at rusqlite's crate root, unlike `Connection` — a real consumer's source, proven
8092 // against a compiling fixture, must use these, not the short forms above).
8093 assert_eq!(classify("rusqlite", "rusqlite::backup::Backup::new"), Some("Db"));
8094 assert_eq!(classify("rusqlite", "rusqlite::blob::Blob::read_at"), Some("Db"));
8095 assert_eq!(classify("rusqlite", "rusqlite::auto_extension::init_auto_extension"), Some("Db"));
8096 // NO FABRICATION: `InnerConnection`/`RawStatement` live in PRIVATE modules (lib.rs `mod
8097 // inner_connection;`/`mod raw_statement;`, no `pub`) — no external consumer can ever name either
8098 // type, so no rule was added despite self-scan flagging them too.
8099 assert_eq!(classify("rusqlite", "rusqlite::InnerConnection::close"), None);
8100 assert_eq!(classify("rusqlite", "rusqlite::RawStatement::step"), None);
8101 // …but `open` stays rusqlite-only (postgres has no open; nothing else may borrow it):
8102 assert_eq!(classify("postgres", "postgres::Client::open"), None);
8103 assert_eq!(classify("tokio_postgres", "tokio_postgres::Client::query_typed"), Some("Db"));
8104 // THE FIX: `Config::connect_raw` does the same protocol handshake as `Config::connect`, over a
8105 // caller-supplied stream instead of one it dials itself.
8106 assert_eq!(classify("tokio_postgres", "tokio_postgres::Config::connect_raw"), Some("Db"));
8107 // diesel's LIMIT-1 + streaming executions; sqlx's multi-result stream:
8108 assert_eq!(classify("diesel", "diesel::RunQueryDsl::first"), Some("Db"));
8109 assert_eq!(classify("diesel", "diesel::RunQueryDsl::load_iter"), Some("Db"));
8110 // THE FIX: `Connection::establish` is diesel's own name for `::connect` — the crate's canonical,
8111 // most-used connection entry point (`SqliteConnection::establish(url)` in every diesel
8112 // quickstart), implemented by really opening the backend handle — but shared no verb spelling
8113 // with `::connect`, so it read pure with zero `coverage.uncovered` disclosure (diesel is
8114 // calibrated, so the miss reads as reviewed-pure, not as a gap).
8115 assert_eq!(classify("diesel", "diesel::sqlite::SqliteConnection::establish"), Some("Db"));
8116 assert_eq!(classify("diesel", "diesel::pg::PgConnection::establish"), Some("Db"));
8117 assert_eq!(classify("diesel", "diesel::mysql::MysqlConnection::establish"), Some("Db"));
8118 assert_eq!(classify("diesel", "diesel::Connection::establish"), Some("Db"));
8119 // NO FABRICATION: diesel's own private plumbing shares the substring but not the exact verb.
8120 assert_eq!(classify("diesel", "diesel::sqlite::SqliteConnection::establish_inner"), None);
8121 assert_eq!(classify("sqlx", "sqlx::query::Query::fetch_many"), Some("Db"));
8122 // sqlx's bare `query()` builder must STAY pure (the original sqlx lesson):
8123 assert_eq!(classify("sqlx", "sqlx::query"), None);
8124 // tracing: the emit/span-lifecycle dispatch is Log; the pure DATA-type accessors are not
8125 // (whole-crate Log fabricated Log on `Level::as_str` / `Span::is_disabled` — the data types are
8126 // pure, same principle as the `log` facade).
8127 assert_eq!(classify("tracing", "tracing::event"), Some("Log"));
8128 assert_eq!(classify("tracing", "tracing::Span::new_span"), Some("Log"));
8129 assert_eq!(classify("tracing", "tracing::Span::record"), Some("Log"));
8130 assert_eq!(classify("tracing", "tracing::Span::enter"), Some("Log"));
8131 assert_eq!(classify("tracing", "tracing::Level::as_str"), None); // pure accessor
8132 assert_eq!(classify("tracing", "tracing::Span::is_disabled"), None); // pure state read
8133 assert_eq!(classify("tracing", "tracing::Span::metadata"), None); // pure accessor
8134 assert_eq!(classify("tracing", "tracing::metadata::Level::TRACE"), None); // pure data type
8135 assert_eq!(classify("tracing", "tracing::field::Field::name"), None); // pure data type
8136 // git2 (high-level Rust API, not the `raw::git_*` FFI tier tested above): remote verbs are Net.
8137 assert_eq!(classify("git2", "git2::Remote::fetch"), Some("Net"));
8138 assert_eq!(classify("git2", "git2::Remote::push"), Some("Net"));
8139 assert_eq!(classify("git2", "git2::Remote::download"), Some("Net"));
8140 assert_eq!(classify("git2", "git2::Remote::connect"), Some("Net"));
8141 assert_eq!(classify("git2", "git2::Remote::connect_auth"), Some("Net"));
8142 assert_eq!(classify("git2", "git2::Remote::ls"), Some("Net"));
8143 assert_eq!(classify("git2", "git2::Remote::upload"), Some("Net"));
8144 // THE COVERAGE-GATE SWEEP FIX: `Repository::open`/`init` (and every other local .git-directory
8145 // operation below) used to read PURE here — the comment this replaced called that "honest, no
8146 // network", but candor-scan self-scanning git2's OWN source (eval/coverage-gate) proved these
8147 // reach a REAL Fs effect (`raw::git_repository_open`/`_init_ext`, already in this file's FFI-tier
8148 // table) that a real consumer's call to the safe wrapper could never surface, because THIS
8149 // block's early `return None` sat in front of that table for every `crate_name == "git2"` call.
8150 // "local, no network" was true and beside the point: Fs was the effect being missed. See MEMORY /
8151 // the commit that added this block for the full sweep (Config/Index/Odb/PackBuilder/Reference/
8152 // Repository/TreeBuilder — every FQN below calls a listed FS FFI leaf; verified against git2
8153 // 0.20.4 source).
8154 assert_eq!(classify("git2", "git2::Repository::open"), Some("Fs"));
8155 assert_eq!(classify("git2", "git2::Repository::init"), Some("Fs"));
8156 assert_eq!(classify("git2", "git2::Repository::open_bare"), Some("Fs"));
8157 assert_eq!(classify("git2", "git2::Repository::open_ext"), Some("Fs"));
8158 assert_eq!(classify("git2", "git2::Repository::open_from_env"), Some("Fs"));
8159 assert_eq!(classify("git2", "git2::Repository::discover"), Some("Fs"));
8160 assert_eq!(classify("git2", "git2::Repository::discover_path"), Some("Fs"));
8161 assert_eq!(classify("git2", "git2::Repository::init_bare"), Some("Fs"));
8162 assert_eq!(classify("git2", "git2::Repository::init_opts"), Some("Fs"));
8163 assert_eq!(classify("git2", "git2::Repository::blob_path"), Some("Fs"));
8164 assert_eq!(classify("git2", "git2::Repository::checkout_head"), Some("Fs"));
8165 assert_eq!(classify("git2", "git2::Repository::checkout_index"), Some("Fs"));
8166 assert_eq!(classify("git2", "git2::Repository::checkout_tree"), Some("Fs"));
8167 assert_eq!(classify("git2", "git2::Repository::commit"), Some("Fs"));
8168 assert_eq!(classify("git2", "git2::Repository::reference"), Some("Fs"));
8169 assert_eq!(classify("git2", "git2::Repository::tag"), Some("Fs"));
8170 assert_eq!(classify("git2", "git2::Config::add_file"), Some("Fs"));
8171 assert_eq!(classify("git2", "git2::Config::open"), Some("Fs"));
8172 assert_eq!(classify("git2", "git2::Config::open_default"), Some("Fs"));
8173 assert_eq!(classify("git2", "git2::Index::add_all"), Some("Fs"));
8174 assert_eq!(classify("git2", "git2::Index::add_path"), Some("Fs"));
8175 assert_eq!(classify("git2", "git2::Index::read"), Some("Fs"));
8176 assert_eq!(classify("git2", "git2::Index::write"), Some("Fs"));
8177 assert_eq!(classify("git2", "git2::Index::write_tree"), Some("Fs"));
8178 assert_eq!(classify("git2", "git2::Index::write_tree_to"), Some("Fs"));
8179 assert_eq!(classify("git2", "git2::Odb::read"), Some("Fs"));
8180 assert_eq!(classify("git2", "git2::Odb::reader"), Some("Fs"));
8181 assert_eq!(classify("git2", "git2::Odb::write"), Some("Fs"));
8182 assert_eq!(classify("git2", "git2::Odb::writer"), Some("Fs"));
8183 assert_eq!(classify("git2", "git2::PackBuilder::write"), Some("Fs"));
8184 assert_eq!(classify("git2", "git2::Reference::delete"), Some("Fs"));
8185 assert_eq!(classify("git2", "git2::Reference::set_target"), Some("Fs"));
8186 assert_eq!(classify("git2", "git2::TreeBuilder::write"), Some("Fs"));
8187 // `Remote::list`/`RemoteConnection::list` call `raw::git_remote_ls` directly — the same
8188 // reference-advertisement Net effect `Remote::ls` was meant to catch, under the name git2 0.20
8189 // actually uses (there is no `ls` method left in the crate at all — that suffix rule is dead
8190 // weight, kept rather than removed since deleting it is a separate, unrelated cleanup).
8191 assert_eq!(classify("git2", "git2::Remote::list"), Some("Net"));
8192 assert_eq!(classify("git2", "git2::RemoteConnection::list"), Some("Net"));
8193 // `Cred::credential_helper` spawns a real `sh -c "<helper> get"` subprocess to resolve
8194 // `credential.helper` config (cred.rs:370-390) — a real consumer's most common way to honor it.
8195 assert_eq!(classify("git2", "git2::Cred::credential_helper"), Some("Exec"));
8196 assert_eq!(classify("git2", "git2::CredentialHelper::execute"), Some("Exec"));
8197 // NO FABRICATION controls: a `write`/`read`/`open` on some OTHER git2 type (not FQN-listed above)
8198 // must stay pure — this fix is FQN-exact, not a suffix re-widen.
8199 assert_eq!(classify("git2", "git2::Repository::state"), None);
8200 assert_eq!(classify("git2", "git2::Blob::content"), None);
8201 // tonic's SERVER half (the coverage-gate sweep found only the client verbs were covered):
8202 // `Router::serve`/`::serve_with_shutdown` bind a real listening socket via `TcpIncoming::new`
8203 // (`StdTcpListener::bind`), verified against tonic 0.12.3 source.
8204 assert_eq!(classify("tonic", "tonic::transport::server::Router::serve"), Some("Net"));
8205 assert_eq!(
8206 classify("tonic", "tonic::transport::server::Router::serve_with_shutdown"),
8207 Some("Net")
8208 );
8209 assert_eq!(classify("tonic", "tonic::TcpIncoming::new"), Some("Net"));
8210 // THIS CONTROL WAS STALE, SOUNDNESS R1528 (R1594's lane). It asserted `None` on the premise that `Server<L>::
8211 // serve_with_shutdown` (mod.rs:530 at 0.12.x) is a DIFFERENT, `pub(crate)` method that no consumer can reach, so a rule
8212 // there would fabricate. Two things made that wrong: tonic 0.14.6 made `Server::{serve, serve_with_shutdown}` PUBLIC
8213 // (mod.rs:663,678; EXECUTED: both bind and listen), and — independent of version — the scan attributes every chain hop
8214 // after `Server::builder()` to the ROOT type, so the quickstart `Server::builder().add_service(svc).serve_with_shutdown(
8215 // addr, f)` reaches classify as `Server::serve_with_shutdown` although at runtime it is `Router`'s. The `None` therefore
8216 // asserted that the quickstart is silent. It is a real listener either way, so the spelling is charged (`tonic_r1528`).
8217 assert_eq!(classify("tonic", "tonic::transport::server::Server::serve_with_shutdown"), Some("Net"));
8218 assert_eq!(classify("tonic", "tonic::transport::Server::serve_with_shutdown"), Some("Net"));
8219 // THE FIX: `Repository::clone`/`clone_recurse` and `RepoBuilder::clone` ARE libgit2's real
8220 // network clone — a corpus round found `git2::Repository::clone(url, path)` reporting ZERO
8221 // effects and passing `deny Net` at exit 0, because the bare `::clone` denylist meant to keep
8222 // out `Remote`'s derived `Clone` dup (below) also excluded the one thing the old comment named
8223 // as NOT meant to be excluded.
8224 assert_eq!(classify("git2", "git2::Repository::clone"), Some("Net"));
8225 assert_eq!(classify("git2", "git2::Repository::clone_recurse"), Some("Net"));
8226 assert_eq!(classify("git2", "git2::build::RepoBuilder::clone"), Some("Net"));
8227 // THE CONTROL: `Remote::clone` is the DERIVED `Clone` trait impl (an Arc/handle duplication, no
8228 // libgit2 call) — it must stay pure. Re-fabricating Net here would undo the very over-charge the
8229 // original exclusion existed to prevent. FQN-exact matching (not a bare `::clone` re-widen) keeps
8230 // this pure while catching `Repository::clone` above.
8231 assert_eq!(classify("git2", "git2::Remote::clone"), None);
8232 // THE SWEEP FIX: a fourth+fifth true positive in the SAME gap — `Submodule::clone`/
8233 // `Submodule::update` call `raw::git_submodule_clone`/`git_submodule_update` directly (the exact
8234 // leaves already in this file's FFI-tier NET table), but git2's documented submodule-init idiom
8235 // calls the safe Rust wrapper, which carried no rule of its own.
8236 assert_eq!(classify("git2", "git2::Submodule::clone"), Some("Net"));
8237 assert_eq!(classify("git2", "git2::Submodule::update"), Some("Net"));
8238 // NO FABRICATION: git2's many pure `update_*` setters (CheckoutBuilder/DiffOptions/StatusOptions)
8239 // must not gain Net — FQN-exact, not a bare `::update` substring.
8240 assert_eq!(classify("git2", "git2::build::CheckoutBuilder::update_only"), None);
8241 assert_eq!(classify("git2", "git2::DiffOptions::update_index"), None);
8242 assert_eq!(classify("git2", "git2::Submodule::update_strategy"), None);
8243 // memmap2: only the syscall-issuing map/flush/protect verbs are Fs; reads over an already-mapped
8244 // region (len/as_ptr/is_empty) and the request builder are PURE (whole-crate Fs fabricated Fs).
8245 assert_eq!(classify("memmap2", "memmap2::MmapOptions::map"), Some("Fs"));
8246 assert_eq!(classify("memmap2", "memmap2::MmapOptions::map_mut"), Some("Fs"));
8247 assert_eq!(classify("memmap2", "memmap2::Mmap::flush"), Some("Fs"));
8248 assert_eq!(classify("memmap2", "memmap2::MmapMut::make_read_only"), Some("Fs"));
8249 assert_eq!(classify("memmap2", "memmap2::Mmap::len"), None); // length read — pure
8250 assert_eq!(classify("memmap2", "memmap2::Mmap::is_empty"), None); // pure
8251 assert_eq!(classify("memmap2", "memmap2::Mmap::as_ptr"), None); // pointer — pure
8252 assert_eq!(classify("memmap2", "memmap2::MmapOptions::new"), None); // request builder — pure
8253 // arboard: the Clipboard handle's read/write verbs are Clipboard; `arboard::Error` formatting
8254 // and option data types are PURE (whole-crate Clipboard fabricated Clipboard on `Error::to_string`).
8255 assert_eq!(classify("arboard", "arboard::Clipboard::new"), Some("Clipboard"));
8256 assert_eq!(classify("arboard", "arboard::Clipboard::get_text"), Some("Clipboard"));
8257 assert_eq!(classify("arboard", "arboard::Clipboard::set_text"), Some("Clipboard"));
8258 assert_eq!(classify("arboard", "arboard::Clipboard::clear"), Some("Clipboard"));
8259 assert_eq!(classify("arboard", "arboard::Error::to_string"), None); // error formatting — pure
8260 assert_eq!(classify("arboard", "arboard::Error::fmt"), None); // Display impl — pure
8261 assert_eq!(classify("arboard", "arboard::ImageData::to_owned_img"), None); // pure data type
8262 // R59-class audit: `file_list` is `Get`/`Set`'s sibling terminal beside `text`/`image`/`html`
8263 // (verified against arboard 3.6.1 — the only `file_list` in the crate, so no ambiguity survives
8264 // the crate gate); `Clear::default` is `clear()`'s own alternate entry point.
8265 assert_eq!(classify("arboard", "arboard::Get::file_list"), Some("Clipboard"));
8266 assert_eq!(classify("arboard", "arboard::Set::file_list"), Some("Clipboard"));
8267 assert_eq!(classify("arboard", "arboard::Clear::default"), Some("Clipboard"));
8268 // fastrand: value draws + entropy-seeded entry points are Rand; the DETERMINISTIC seeded ctor
8269 // `with_seed` and state split/copy (`fork`/`clone`) are PURE (whole-crate Rand fabricated Rand).
8270 assert_eq!(classify("fastrand", "fastrand::u32"), Some("Rand")); // top-level draw
8271 assert_eq!(classify("fastrand", "fastrand::Rng::usize"), Some("Rand"));
8272 assert_eq!(classify("fastrand", "fastrand::Rng::shuffle"), Some("Rand"));
8273 assert_eq!(classify("fastrand", "fastrand::Rng::new"), Some("Rand")); // entropy-seeded
8274 assert_eq!(classify("fastrand", "fastrand::Rng::with_seed"), None); // deterministic ctor — pure
8275 assert_eq!(classify("fastrand", "fastrand::Rng::fork"), None); // state split — pure
8276 assert_eq!(classify("fastrand", "fastrand::Rng::clone"), None); // state copy — pure
8277 // portable_pty / async_process: spawn/wait keep Exec; config GETTERS and pure data ctors/setters
8278 // do NOT (base Exec fabricated on `CommandBuilder::get_cwd` / `PtySize::default` / `Stdio::piped`).
8279 assert_eq!(classify("portable_pty", "portable_pty::PtySystem::openpty"), Some("Exec"));
8280 assert_eq!(classify("portable_pty", "portable_pty::SlavePty::spawn_command"), Some("Exec"));
8281 assert_eq!(classify("portable_pty", "portable_pty::CommandBuilder::get_argv"), None); // getter
8282 assert_eq!(classify("portable_pty", "portable_pty::CommandBuilder::get_cwd"), None); // getter
8283 assert_eq!(classify("portable_pty", "portable_pty::PtySize::default"), None); // pure data type
8284 // `CommandBuilder::new` snapshots the process environment (b11, EXECUTED) — Env; see `r1528_b11_rows_classify`.
8285 assert_eq!(classify("async_process", "async_process::Command::spawn"), Some("Exec"));
8286 assert_eq!(classify("async_process", "async_process::Command::output"), Some("Exec"));
8287 assert_eq!(classify("async_process", "async_process::Stdio::piped"), None); // pure data type
8288 assert_eq!(classify("async_process", "async_process::Stdio::null"), None); // pure data type
8289 // FFI tiers (matched by distinctive leaf, alias-independent)
8290 assert_eq!(classify("libc", "libc::open"), Some("Fs"));
8291 assert_eq!(classify("libc", "libc::connect"), Some("Net"));
8292 assert_eq!(classify("libc", "libc::read"), None); // generic fd op — deliberately unclassified
8293 assert_eq!(classify("ffi", "ffi::sqlite3_step"), Some("Db"));
8294 // SOUNDNESS R166 — the PROCESS-GLOBAL extension registry. `rusqlite::register_auto_extension`
8295 // calls `ffi::sqlite3_auto_extension` directly and read `[]` on the 0.34.0 binary: an explicit
8296 // empty effect set over a call that changes what every LATER `sqlite3_open` executes.
8297 assert_eq!(classify("ffi", "ffi::sqlite3_auto_extension"), Some("Db"));
8298 assert_eq!(classify("ffi", "ffi::sqlite3_cancel_auto_extension"), Some("Db"));
8299 assert_eq!(classify("ffi", "ffi::sqlite3_reset_auto_extension"), Some("Db"));
8300 assert_eq!(classify("ffi", "ffi::sqlite3_enable_load_extension"), Some("Db"));
8301 // …and the database-CONTENT I/O the same sweep found unlisted beside it.
8302 assert_eq!(classify("ffi", "ffi::sqlite3_deserialize"), Some("Db"));
8303 assert_eq!(classify("ffi", "ffi::sqlite3_serialize"), Some("Db"));
8304 assert_eq!(classify("ffi", "ffi::sqlite3_db_cacheflush"), Some("Db"));
8305 assert_eq!(classify("ffi", "ffi::sqlite3_file_control"), Some("Db"));
8306 assert_eq!(classify("ffi", "ffi::sqlite3_wal_autocheckpoint"), Some("Db"));
8307 // CONTROLS for the boundary this list deliberately stops at — an in-memory accessor and a
8308 // per-connection callback INSTALLER stay unclassified, so the additions above are not read as
8309 // "every `sqlite3_*` is Db". Both pass before and after R166; recorded as boundary, not coverage.
8310 assert_eq!(classify("ffi", "ffi::sqlite3_column_int64"), None);
8311 assert_eq!(classify("ffi", "ffi::sqlite3_create_function_v2"), None);
8312 assert_eq!(classify("raw", "raw::git_remote_fetch"), Some("Net"));
8313 // libgit2 clone + submodule clone/update fetch over the network (an A/B on git2 0.20 caught
8314 // `Submodule::update`/`clone` and `Repository::clone` reporting no Net — the latter because the
8315 // `src/build.rs` module was being dropped as if it were the Cargo build script).
8316 assert_eq!(classify("raw", "raw::git_clone"), Some("Net"));
8317 assert_eq!(classify("raw", "raw::git_submodule_clone"), Some("Net"));
8318 assert_eq!(classify("raw", "raw::git_submodule_update"), Some("Net"));
8319 assert_eq!(classify("raw", "raw::git_submodule_open"), None); // local subrepo open — not Net
8320 // libcurl: the transfer/raw-socket entry points are Net (an A/B on curl 0.4 caught the whole
8321 // crate reporting ZERO Net); the big setopt/init/getinfo surface — and the readiness-wait
8322 // multi_wait/poll — stay unclassified (the loop's perform is the boundary).
8323 assert_eq!(classify("curl_sys", "curl_sys::curl_easy_perform"), Some("Net"));
8324 assert_eq!(classify("curl_sys", "curl_sys::curl_easy_send"), Some("Net"));
8325 assert_eq!(classify("curl_sys", "curl_sys::curl_multi_perform"), Some("Net"));
8326 assert_eq!(classify("curl_sys", "curl_sys::curl_multi_socket_action"), Some("Net"));
8327 assert_eq!(classify("curl_sys", "curl_sys::curl_easy_setopt"), None); // in-memory option write
8328 assert_eq!(classify("curl_sys", "curl_sys::curl_easy_init"), None); // handle alloc
8329 assert_eq!(classify("curl_sys", "curl_sys::curl_multi_wait"), None); // readiness wait, no payload
8330 // consumer-side `curl` crate rule: the dispatch verbs are Net, the setopt builders pure.
8331 assert_eq!(classify("curl", "curl::easy::Easy::perform"), Some("Net"));
8332 assert_eq!(classify("curl", "curl::multi::Multi::perform"), Some("Net"));
8333 assert_eq!(classify("curl", "curl::easy::Easy::send"), Some("Net"));
8334 assert_eq!(classify("curl", "curl::easy::Easy::url"), None); // CURLOPT setter — pure
8335 assert_eq!(classify("curl", "curl::easy::Easy::timeout"), None); // pure setter; Multi::timeout under-reported by design
8336 assert_eq!(classify("ffi", "ffi::SSL_connect"), Some("Net"));
8337 // pure crates stay pure
8338 assert_eq!(classify("serde", "serde::Serialize::serialize"), None);
8339 assert_eq!(classify("std", "std::vec::Vec::push"), None);
8340
8341 // ── sweep 2026-06-17: fabrication carve-outs + DNS coverage (each fails pre-fix) ──
8342 // [24] std::net socket accessors are pure; the I/O verbs stay Net.
8343 assert_eq!(classify("std", "std::net::TcpStream::connect"), Some("Net"));
8344 assert_eq!(classify("std", "std::net::TcpStream::local_addr"), None);
8345 assert_eq!(classify("std", "std::net::TcpStream::nodelay"), None);
8346 assert_eq!(classify("std", "std::net::TcpStream::ttl"), None);
8347 assert_eq!(classify("std", "std::net::UdpSocket::peer_addr"), None);
8348 // [37] std DNS resolution is Net (was floored).
8349 assert_eq!(classify("std", "std::net::lookup_host"), Some("Net"));
8350 assert_eq!(classify("std", "core::net::ToSocketAddrs::to_socket_addrs"), Some("Net"));
8351 // [23] std::process getters are pure; spawn/new stay Exec.
8352 assert_eq!(classify("std", "std::process::Command::get_program"), None);
8353 assert_eq!(classify("std", "std::process::Command::get_args"), None);
8354 assert_eq!(classify("std", "std::process::Child::id"), None);
8355 assert_eq!(classify("std", "std::process::Command::spawn"), Some("Exec"));
8356 // [27] redis ConnectionManager::clone is an Arc bump (pure); a query round-trips.
8357 assert_eq!(classify("redis", "redis::aio::ConnectionManager::clone"), None);
8358 assert_eq!(classify("redis", "redis::aio::ConnectionManager::send_packed_command"), Some("Db"));
8359 // [5] sea_orm re-exported sea_query builder algebra is pure; execution verbs stay Db.
8360 assert_eq!(classify("sea_orm", "sea_orm::sea_query::Func::count"), None);
8361 assert_eq!(classify("sea_orm", "sea_orm::sea_query::Condition::all"), None);
8362 assert_eq!(classify("sea_orm", "sea_orm::Select::all"), Some("Db"));
8363 // THE FIX: `Database::connect_proxy` (the `proxy`-feature sibling of `Database::connect`) opens
8364 // a live `DatabaseConnection` through a caller-supplied `ProxyDatabaseTrait` — same effect,
8365 // missing from an allowlist keyed on the plain `connect` spelling.
8366 assert_eq!(classify("sea_orm", "sea_orm::Database::connect_proxy"), Some("Db"));
8367 // THE COVERAGE-GATE SWEEP: the transaction and pagination families, verified against sea-orm
8368 // 1.1.20 source.
8369 assert_eq!(classify("sea_orm", "sea_orm::DatabaseConnection::transaction"), Some("Db"));
8370 assert_eq!(
8371 classify("sea_orm", "sea_orm::DatabaseConnection::transaction_with_config"),
8372 Some("Db")
8373 );
8374 // BONUS (found proving the above reachable with a real fixture, not in the original ratchet):
8375 assert_eq!(classify("sea_orm", "sea_orm::DatabaseConnection::ping"), Some("Db"));
8376 assert_eq!(classify("sea_orm", "sea_orm::DatabaseConnection::begin"), Some("Db"));
8377 assert_eq!(
8378 classify("sea_orm", "sea_orm::DatabaseConnection::begin_with_config"),
8379 Some("Db")
8380 );
8381 assert_eq!(classify("sea_orm", "sea_orm::DatabaseTransaction::commit"), Some("Db"));
8382 assert_eq!(classify("sea_orm", "sea_orm::DatabaseTransaction::rollback"), Some("Db"));
8383 assert_eq!(classify("sea_orm", "sea_orm::SqlxMySqlPoolConnection::begin"), Some("Db"));
8384 assert_eq!(classify("sea_orm", "sea_orm::SqlxMySqlPoolConnection::ping"), Some("Db"));
8385 assert_eq!(classify("sea_orm", "sea_orm::SqlxMySqlPoolConnection::transaction"), Some("Db"));
8386 assert_eq!(classify("sea_orm", "sea_orm::SqlxPostgresPoolConnection::begin"), Some("Db"));
8387 assert_eq!(classify("sea_orm", "sea_orm::SqlxSqlitePoolConnection::ping"), Some("Db"));
8388 assert_eq!(classify("sea_orm", "sea_orm::Paginator::fetch"), Some("Db"));
8389 assert_eq!(classify("sea_orm", "sea_orm::Paginator::fetch_and_next"), Some("Db"));
8390 assert_eq!(classify("sea_orm", "sea_orm::Paginator::into_stream"), Some("Db"));
8391 assert_eq!(classify("sea_orm", "sea_orm::Paginator::num_pages"), Some("Db"));
8392 assert_eq!(classify("sea_orm", "sea_orm::Paginator::num_items_and_pages"), Some("Db"));
8393 assert_eq!(classify("sea_orm", "sea_orm::Insert::exec_with_returning_keys"), Some("Db"));
8394 assert_eq!(classify("sea_orm", "sea_orm::Inserter::exec_with_returning_many"), Some("Db"));
8395 assert_eq!(classify("sea_orm", "sea_orm::TryInsert::exec_with_returning_keys"), Some("Db"));
8396 // NO FABRICATION: the mock/proxy backends share every one of these verb names but perform no
8397 // real I/O (mock) or dispatch through a caller-supplied trait (proxy) — FQN-exact, not a bare
8398 // `::ping`/`::begin`/`::transaction`/`::commit`/`::rollback` suffix, keeps them pure/unknown.
8399 assert_eq!(classify("sea_orm", "sea_orm::MockDatabaseConnection::ping"), None);
8400 assert_eq!(classify("sea_orm", "sea_orm::MockDatabaseConnection::begin"), None);
8401 assert_eq!(classify("sea_orm", "sea_orm::ProxyDatabaseConnection::commit"), None);
8402 assert_eq!(classify("sea_orm", "sea_orm::ProxyDatabaseConnection::ping"), None);
8403 }
8404
8405 #[test]
8406 fn lettre_tls_setup_and_connection_family_is_covered() {
8407 // THE COVERAGE-GATE SWEEP, verified against lettre 0.11.23 source. `build_rustls` calls
8408 // `rustls_native_certs::load_native_certs()` (a real OS-trust-store read) + `tracing::debug!`.
8409 assert_eq!(
8410 classify("lettre", "lettre::transport::smtp::client::TlsParametersBuilder::build_rustls"),
8411 Some("Fs")
8412 );
8413 assert_eq!(classify("lettre", "lettre::TlsParametersBuilder::build"), Some("Fs"));
8414 assert_eq!(classify("lettre", "lettre::TlsParameters::new"), Some("Fs"));
8415 assert_eq!(classify("lettre", "lettre::TlsParameters::new_rustls"), Some("Fs"));
8416 assert_eq!(classify("lettre", "lettre::SmtpTransport::from_url"), Some("Fs"));
8417 assert_eq!(classify("lettre", "lettre::SmtpTransport::relay"), Some("Fs"));
8418 assert_eq!(classify("lettre", "lettre::SmtpTransport::starttls_relay"), Some("Fs"));
8419 assert_eq!(classify("lettre", "lettre::AsyncSmtpTransport::from_url"), Some("Fs"));
8420 assert_eq!(classify("lettre", "lettre::AsyncSmtpTransport::relay"), Some("Fs"));
8421 assert_eq!(classify("lettre", "lettre::AsyncSmtpTransport::starttls_relay"), Some("Fs"));
8422 assert_eq!(classify("lettre", "lettre::FileTransport::read"), Some("Fs"));
8423 // The sealed `Executor` trait impl — `#[doc(hidden)]`, a narrower surface, but genuinely
8424 // reachable (both the trait and `AsyncStd1Executor` are `pub`, re-exported at the crate root).
8425 assert_eq!(classify("lettre", "lettre::AsyncStd1Executor::connect"), Some("Net"));
8426 assert_eq!(classify("lettre", "lettre::AsyncStd1Executor::fs_read"), Some("Fs"));
8427 assert_eq!(classify("lettre", "lettre::AsyncStd1Executor::fs_write"), Some("Fs"));
8428 // Only reachable via the module-qualified path — neither type is re-exported at the crate root.
8429 assert_eq!(
8430 classify(
8431 "lettre",
8432 "lettre::transport::smtp::client::AsyncSmtpConnection::connect_asyncstd1"
8433 ),
8434 Some("Net")
8435 );
8436 assert_eq!(
8437 classify(
8438 "lettre",
8439 "lettre::transport::smtp::client::AsyncNetworkStream::connect_asyncstd1"
8440 ),
8441 Some("Net")
8442 );
8443 // NO FABRICATION: `NetworkStream` lives in a PRIVATE module (`mod net;`, never re-exported) — no
8444 // external consumer can ever name it, so no rule was added despite self-scan flagging its
8445 // `shutdown`/`set_read_timeout`/`set_write_timeout` (propagated from the coarse
8446 // `std::net::TcpStream` whole-handle rule).
8447 assert_eq!(classify("lettre", "lettre::NetworkStream::shutdown"), None);
8448 assert_eq!(classify("lettre", "lettre::NetworkStream::set_read_timeout"), None);
8449 }
8450
8451 #[test]
8452 fn rand_osrng_handle_ops_are_pure_but_draws_are_rand() {
8453 // Adversarial-review fabrication: the blanket `contains("OsRng")` tagged `OsRng::clone` Rand,
8454 // but OsRng is a unit struct — clone/fork/default draw no entropy. The real draws still fire.
8455 assert_eq!(classify("rand", "rand::rngs::OsRng::clone"), None);
8456 assert_eq!(classify("rand", "rand::rngs::OsRng::default"), None);
8457 assert_eq!(classify("rand", "rand::rngs::OsRng::fill_bytes"), Some("Rand")); // a real draw
8458 assert_eq!(classify("rand", "rand::rngs::OsRng::next_u32"), Some("Rand"));
8459 assert_eq!(classify("rand", "rand::Rng::gen"), Some("Rand")); // verb path unaffected
8460 assert_eq!(classify("rand", "rand::distributions::Uniform::new"), None); // pure ctor still pure
8461 }
8462
8463 #[test]
8464 fn redis_connection_manager_config_builder_is_pure() {
8465 // Adversarial-review fabrication: `contains("ConnectionManager")` hit the pure *Config* builder.
8466 assert_eq!(classify("redis", "redis::aio::ConnectionManagerConfig::new"), None);
8467 assert_eq!(classify("redis", "redis::aio::ConnectionManagerConfig::set_max_delay"), None);
8468 // the LIVE manager still round-trips (Db).
8469 assert_eq!(classify("redis", "redis::aio::ConnectionManager::new"), Some("Db"));
8470 assert_eq!(classify("redis", "redis::Commands::get"), Some("Db"));
8471 }
8472
8473 #[test]
8474 fn redis_get_connection_info_is_a_pure_accessor_not_swept_by_the_substring() {
8475 // coverage-gate sweep (2026-08-27): `contains("::get_connection")` also matched
8476 // `Client::get_connection_info` (a pure accessor over an already-stored field) because it is a
8477 // literal substring of that name — the real connection-getters stay caught.
8478 assert_eq!(classify("redis", "redis::Client::get_connection_info"), None);
8479 assert_eq!(classify("redis", "redis::Client::get_connection"), Some("Db"));
8480 assert_eq!(classify("redis", "redis::Client::get_async_connection"), Some("Db"));
8481 assert_eq!(
8482 classify("redis", "redis::Client::get_multiplexed_async_connection"),
8483 Some("Db")
8484 );
8485 }
8486
8487 #[test]
8488 fn aws_config_load_from_env_is_loads_own_convenience_wrapper() {
8489 // THE FIX: `load_from_env()` is the crate's OWN "convenience wrapper" (its doc comment's words)
8490 // around `from_env().load().await` — the exact already-modelled effect one call down — but
8491 // "load_from_env".ends_with("::load") is false.
8492 assert_eq!(classify("aws_config", "aws_config::load"), Some("Net"));
8493 assert_eq!(classify("aws_config", "aws_config::load_defaults"), Some("Net"));
8494 assert_eq!(classify("aws_config", "aws_config::load_from_env"), Some("Net"));
8495 assert_eq!(classify("aws_config", "aws_config::from_env"), None); // pure builder
8496 }
8497
8498 #[test]
8499 fn tungstenite_stream_first_handshake_entry_points_are_net() {
8500 // THE FIX: `client`/`client_with_config`/`client_tls`/`client_tls_with_config` (client.rs:176,
8501 // 159; tls.rs:163,179) and `accept`/`accept_with_config`/`accept_hdr`/`accept_hdr_with_config`
8502 // (server.rs:23-63) each run `{Client,Server}Handshake::start(stream, ..).handshake()` — the real
8503 // WS upgrade I/O over an already-open stream. tungstenite's OWN documented way to run over a
8504 // caller-managed TCP/TLS/mio stream, missing from a verb list keyed only on `connect`.
8505 assert_eq!(classify("tungstenite", "tungstenite::client::client"), Some("Net"));
8506 assert_eq!(classify("tungstenite", "tungstenite::client::client_with_config"), Some("Net"));
8507 assert_eq!(classify("tungstenite", "tungstenite::tls::client_tls"), Some("Net"));
8508 assert_eq!(classify("tungstenite", "tungstenite::tls::client_tls_with_config"), Some("Net"));
8509 assert_eq!(classify("tungstenite", "tungstenite::server::accept"), Some("Net"));
8510 assert_eq!(classify("tungstenite", "tungstenite::server::accept_with_config"), Some("Net"));
8511 assert_eq!(classify("tungstenite", "tungstenite::server::accept_hdr"), Some("Net"));
8512 assert_eq!(classify("tungstenite", "tungstenite::server::accept_hdr_with_config"), Some("Net"));
8513 assert_eq!(classify("tungstenite", "tungstenite::client::connect_with_config"), Some("Net"));
8514 }
8515
8516 #[test]
8517 fn mysql_conn_new_is_the_real_connect() {
8518 // THE FIX: `mysql::Conn::new` (conn/mod.rs:342) calls `connect_stream()?; connect()?` directly;
8519 // `mysql_async::Conn::new` (conn/mod.rs:921) is the async equivalent. Each crate's own primary
8520 // connection constructor, the crate's own first doctest example — but `::new` never appeared in
8521 // the verb list.
8522 assert_eq!(classify("mysql", "mysql::Conn::new"), Some("Db"));
8523 assert_eq!(classify("mysql_async", "mysql_async::Conn::new"), Some("Db"));
8524 // NO FABRICATION: the SAME two crates' unrelated pure `new`s (Opts/PoolConstraints/TxOpts/…)
8525 // must not gain Db — scoped to the `Conn::` segment, not a bare `::new` suffix.
8526 assert_eq!(classify("mysql", "mysql::Opts::new"), None);
8527 assert_eq!(classify("mysql", "mysql::PoolConstraints::new"), None);
8528 assert_eq!(classify("mysql_async", "mysql_async::TxOpts::new"), None);
8529 }
8530
8531 #[test]
8532 fn mongodb_with_options_is_the_same_effect_as_with_uri_str() {
8533 // THE FIX: `with_uri_str` (client.rs:179) is `ClientOptions::parse(uri).await?;
8534 // Client::with_options(options)` one call down — `with_options` (client.rs:188) is where the
8535 // topology/monitoring actually spins up, and mongodb's own doc calls it the entry point for a
8536 // caller who already holds parsed `ClientOptions`. Verified against mongodb 3.8.1 (both the
8537 // async `client.rs` and sync `sync/client.rs` `Client`).
8538 assert_eq!(classify("mongodb", "mongodb::Client::with_uri_str"), Some("Db"));
8539 assert_eq!(classify("mongodb", "mongodb::Client::with_options"), Some("Db"));
8540 assert_eq!(classify("mongodb", "mongodb::sync::Client::with_options"), Some("Db"));
8541 assert_eq!(classify("mongodb", "mongodb::Collection::find_one"), Some("Db"));
8542 // NO FABRICATION: an unrelated crate's `with_options` builder must not gain Db — crate-gated.
8543 assert_eq!(classify("some_other_crate", "some_other_crate::Widget::with_options"), None);
8544 }
8545
8546 #[test]
8547 fn pure_fd_transfer_is_not_an_effect() {
8548 // ADOPTING / EXTRACTING / BORROWING an already-open descriptor (or unwrapping an async type back
8549 // to its std type) issues NO syscall — it must be PURE even though it hangs off a std I/O type
8550 // whose prefix rule would otherwise fire Net/Fs/Ipc. (Real tokio sweep: `into_std`, `from_raw_fd`,
8551 // `as_raw_fd` all fabricated effects.)
8552 assert_eq!(classify("std", "std::net::TcpStream::from_raw_fd"), None);
8553 assert_eq!(classify("std", "std::net::TcpStream::into_raw_fd"), None);
8554 assert_eq!(classify("std", "std::net::TcpStream::as_raw_fd"), None);
8555 assert_eq!(classify("std", "std::net::TcpListener::from_raw_fd"), None);
8556 assert_eq!(classify("std", "std::net::UdpSocket::from_raw_socket"), None);
8557 assert_eq!(classify("std", "std::fs::File::from_raw_fd"), None);
8558 assert_eq!(classify("std", "std::fs::File::into_raw_fd"), None);
8559 assert_eq!(classify("std", "std::fs::File::as_raw_handle"), None);
8560 assert_eq!(classify("std", "std::os::unix::net::UnixStream::from_raw_fd"), None);
8561 // `SocketAddr::from_pathname` builds an address struct, opens no socket — pure. (socket2 sweep.)
8562 assert_eq!(classify("std", "std::os::unix::net::SocketAddr::from_pathname"), None);
8563 assert_eq!(classify("tokio", "tokio::net::TcpStream::from_raw_fd"), None);
8564 assert_eq!(classify("tokio", "tokio::net::TcpStream::into_std"), None); // unwrap → std type, pure
8565 assert_eq!(classify("tokio", "tokio::fs::File::into_std"), None);
8566 // …but a REAL open/connect on the SAME types still fires the effect — the carve-out is leaf-precise.
8567 assert_eq!(classify("std", "std::net::TcpStream::connect"), Some("Net"));
8568 assert_eq!(classify("std", "std::fs::File::open"), Some("Fs"));
8569 assert_eq!(classify("std", "std::fs::read"), Some("Fs"));
8570 assert_eq!(classify("std", "std::os::unix::net::UnixStream::connect"), Some("Ipc"));
8571 assert_eq!(classify("tokio", "tokio::net::TcpStream::connect"), Some("Net"));
8572 // R1288 — tokio's clock, and only its `now`.
8573 assert_eq!(classify("tokio", "tokio::time::Instant::now"), Some("Clock"));
8574 assert_eq!(classify("tokio", "tokio::time::Instant::from_std"), None);
8575 assert_eq!(classify("quanta", "quanta::Instant::now"), Some("Clock"));
8576 assert_eq!(classify("web_time", "web_time::Instant::now"), Some("Clock"));
8577 }
8578
8579 /// THE PLATFORM `fs` MODULES. `std::fs::` was the whole filesystem rule, so every function in
8580 /// `std::os::{unix,windows,wasi}::fs` read PURE — measured with EXECUTED ground truth (a `cargo run`
8581 /// that created a real symlink and stat'd it back), `deny Fs` over a crate whose only filesystem
8582 /// write was `std::os::unix::fs::symlink` exited 0.
8583 ///
8584 /// The controls are the point: the `*Ext` DATA/BUILDER traits must stay pure, or the fix trades a
8585 /// silent under-report for a fabrication on every `m.uid()` — and `FileExt` must NOT, because
8586 /// `read_at`/`write_at` are the positional-I/O syscalls this rule exists to catch.
8587 #[test]
8588 fn the_platform_fs_modules_are_filesystem_io_and_their_data_traits_are_not() {
8589 for p in [
8590 "std::os::unix::fs::symlink", "std::os::unix::fs::chown", "std::os::unix::fs::lchown",
8591 "std::os::unix::fs::fchown", "std::os::unix::fs::chroot",
8592 "std::os::windows::fs::symlink_dir", "std::os::windows::fs::symlink_file",
8593 "std::os::windows::fs::junction_point",
8594 "std::os::unix::fs::FileExt::read_at", "std::os::unix::fs::FileExt::write_at",
8595 "std::os::windows::fs::FileExt::seek_read", "std::os::windows::fs::FileExt::seek_write",
8596 ] {
8597 assert_eq!(classify("std", p), Some("Fs"), "{p} performs filesystem I/O");
8598 }
8599 for p in [
8600 "std::os::unix::fs::MetadataExt::uid", "std::os::unix::fs::MetadataExt::mode",
8601 "std::os::unix::fs::PermissionsExt::mode", "std::os::unix::fs::PermissionsExt::set_mode",
8602 "std::os::unix::fs::OpenOptionsExt::custom_flags",
8603 "std::os::unix::fs::DirBuilderExt::mode",
8604 "std::os::unix::fs::DirEntryExt::ino", "std::os::unix::fs::FileTypeExt::is_fifo",
8605 "std::os::windows::fs::MetadataExt::file_attributes",
8606 ] {
8607 assert_eq!(classify("std", p), None,
8608 "{p} reads or configures data already in hand — charging it would FABRICATE Fs");
8609 }
8610 // `std::os::unix::net` is Ipc and must be untouched by a rule keyed one segment along.
8611 assert_eq!(classify("std", "std::os::unix::net::UnixStream::connect"), Some("Ipc"));
8612 }
8613
8614 /// SOUNDNESS R141 — the platform `process` modules, the sibling `fs` had a rule and `process` did
8615 /// not. `CommandExt::exec` is `execvp`: it REPLACES the process image, ground-truthed by a
8616 /// `cargo run` that printed a line from the exec'd image and never reached the line after the call.
8617 /// Both directions are driven here: the spawn surface must charge Exec (without the rule every
8618 /// assertion in the first loop returns `None` and `deny Exec` over a library whose only function
8619 /// calls `CommandExt::exec` exits 0), and the already-collected-status traits must NOT, or the fix
8620 /// trades a silent under-report for a fabrication on every `status.signal()` — the same control
8621 /// shape as the `fs` test above.
8622 #[test]
8623 fn the_platform_process_modules_are_exec_and_their_status_traits_are_not() {
8624 for p in [
8625 "std::os::unix::process::CommandExt::exec",
8626 "std::os::unix::process::CommandExt::pre_exec",
8627 "std::os::unix::process::CommandExt::arg0",
8628 "std::os::unix::process::CommandExt::uid",
8629 "std::os::unix::process::CommandExt::gid",
8630 "std::os::unix::process::CommandExt::groups",
8631 "std::os::unix::process::CommandExt::process_group",
8632 "std::os::windows::process::CommandExt::creation_flags",
8633 "std::os::windows::process::CommandExt::raw_arg",
8634 "std::os::windows::process::ChildExt::main_thread_handle",
8635 "std::os::wasi::process::CommandExt::exec",
8636 ] {
8637 assert_eq!(classify("std", p), Some("Exec"), "{p} is subprocess machinery");
8638 }
8639 for p in [
8640 "std::os::unix::process::ExitStatusExt::signal",
8641 "std::os::unix::process::ExitStatusExt::core_dumped",
8642 "std::os::unix::process::ExitStatusExt::from_raw",
8643 "std::os::unix::process::ExitStatusExt::into_raw",
8644 "std::os::unix::process::ExitStatusError::signal",
8645 "std::os::windows::process::ExitStatusExt::from_raw",
8646 "std::os::windows::process::ExitCodeExt::from_raw",
8647 "std::os::unix::process::parent_id",
8648 ] {
8649 assert_eq!(classify("std", p), None,
8650 "{p} reads a status already collected (or this process's own pid) — charging it \
8651 would FABRICATE Exec");
8652 }
8653 // Keyed one segment along: `fs` and `net` under the same platform root must be untouched.
8654 assert_eq!(classify("std", "std::os::unix::fs::FileExt::read_at"), Some("Fs"));
8655 assert_eq!(classify("std", "std::os::unix::net::UnixStream::connect"), Some("Ipc"));
8656 }
8657
8658 #[test]
8659 fn command_head_refines_the_exec_cliff() {
8660 use super::classify_command_head as h;
8661 // unambiguous external tools classify by basename (spec §4 ⟨0.5⟩)
8662 assert_eq!(h("curl"), &["Net"]);
8663 assert_eq!(h("telnet"), &["Net"]);
8664 assert_eq!(h("sftp"), &["Net"]);
8665 assert_eq!(h("/usr/local/bin/psql"), &["Db"]); // basename match strips the path
8666 assert_eq!(h("mongo"), &["Db"]);
8667 assert_eq!(h("cqlsh"), &["Db"]);
8668 // a candor engine is Fs/Env — spec-SUPPLIED by §7 item 12, not curation
8669 assert_eq!(h("candor-scan"), &["Env", "Fs"]);
8670 assert_eq!(h("candor-run.sh"), &["Env", "Fs"]);
8671 // an unrecognised head adds nothing — the bare Exec cliff stands (never guess). `make`/`npm`
8672 // run the project's own code; `git`/`rsync` are multi-modal (local vs remote) — all keep the
8673 // cliff rather than fabricate an effect for the common case.
8674 assert_eq!(h("some-unknown-tool"), &[] as &[&str]);
8675 assert_eq!(h("make"), &[] as &[&str]);
8676 assert_eq!(h("npm"), &[] as &[&str]);
8677 assert_eq!(h("git"), &[] as &[&str]);
8678 assert_eq!(h("rsync"), &[] as &[&str]);
8679 // a builder MODIFIER (`.arg`/`.env`) names no program — its literal must NOT refine (a
8680 // whole-crate-Exec crate classifies every method; `.env("psql",..)` must not fabricate Db).
8681 assert!(is_cmd_builder_method("env") && is_cmd_builder_method("arg") && is_cmd_builder_method("current_dir"));
8682 assert!(!is_cmd_builder_method("new")); // Command::new NAMES the program
8683 assert!(!is_cmd_builder_method("cmd")); // duct::cmd NAMES the program
8684 // The gate that ADMITS a literal to classify_command_head is an ALLOWLIST of program-NAMING
8685 // methods, not the builder denylist. Inversion matters: a whole-crate-Exec crate (portable_pty)
8686 // classifies EVERY method as Exec, so a getter like `cmd.get_env("psql")` — absent from the
8687 // builder denylist — would have leaked "psql" to the head and FABRICATED Db. Only `new`/`cmd`
8688 // name a program, so only they may refine.
8689 assert!(is_cmd_naming_method("new") && is_cmd_naming_method("cmd"));
8690 assert!(!is_cmd_naming_method("get_env")); // a GETTER, not a namer — the leak this closes
8691 assert!(!is_cmd_naming_method("arg") && !is_cmd_naming_method("env") && !is_cmd_naming_method("current_dir"));
8692 }
8693
8694 /// SOUNDNESS R379 — THE MASKING GUARD'S ALLOWLIST, AND THE DENYLIST THAT WAS MEASURED AND REJECTED.
8695 ///
8696 /// The verbs here take the HOST as an argument, so a runtime value in that position is invisible to
8697 /// the gate and the surface must be marked incomplete. The DNS resolver verbs were missing, and
8698 /// their absence was a GATE BYPASS, not a missed disclosure: `allow Net good.example.com` exited 0
8699 /// over `r.lookup_ip(caller_host)` sitting beside a benign literal, measured against real
8700 /// hickory-resolver 0.26.2.
8701 /// SOUNDNESS R386 — the `sqlite3_*` C API takes SQL, and its absence let a benign SIBLING literal
8702 /// certify arbitrary caller-supplied SQL.
8703 ///
8704 /// R383 recorded this gap as "REAL BUT NOT LIVE" because a C-string literal never reaches `tables`.
8705 /// That is true of an ISOLATED function and is the WRONG SHAPE: the literal only has to come from
8706 /// the same FUNCTION, not the same call. Measured, calibrated first (`deny Db` exits 1):
8707 /// `conn.execute("INSERT INTO users (id) VALUES (1)", [])` beside `sqlite3_exec(db, caller_sql, …)`
8708 /// gave `tables:['users'] incomplete:NONE` and `allow Db users` exited 0.
8709 #[test]
8710 fn db_query_arg_covers_the_sqlite3_sql_bearing_family() {
8711 // Every spelling that TAKES SQL — the whole family, not the one that was measured (R346).
8712 for f in ["sqlite3_exec", "sqlite3_prepare", "sqlite3_prepare_v2", "sqlite3_prepare_v3",
8713 "sqlite3_prepare16", "sqlite3_prepare16_v2", "sqlite3_prepare16_v3",
8714 // R386's own boundary, found by a release panel: SQL is arg 1 here too.
8715 "sqlite3_get_table"] {
8716 assert!(is_db_query_arg(f), "{f} carries SQL — a runtime query there is invisible (R386)");
8717 }
8718 // THE OVER-MASK CONTROLS, and they carry as much weight as the fix. These take no query, so
8719 // masking them would fail `allow Db` closed over nothing. `sqlite3_open*` is the sharpest: it
8720 // takes a FILENAME, so masking it would claim a Db surface for an Fs locator.
8721 for f in ["sqlite3_step", "sqlite3_close", "sqlite3_open", "sqlite3_open_v2", "sqlite3_open16",
8722 "sqlite3_serialize", "sqlite3_deserialize", "sqlite3_finalize", "sqlite3_reset",
8723 // the backup/blob handle verbs carry no query either — the other half of the family
8724 "sqlite3_backup_init", "sqlite3_backup_step", "sqlite3_blob_open", "sqlite3_blob_read"] {
8725 assert!(!is_db_query_arg(f),
8726 "{f} carries no SQL — masking it claims an incomplete surface over nothing, and for \
8727 the open* family it would claim a Db surface for a FILENAME");
8728 }
8729 // The pre-existing high-level verbs must survive.
8730 for f in ["execute", "query", "prepare", "prepare_cached", "simple_query"] {
8731 assert!(is_db_query_arg(f), "{f} was already covered and must stay covered");
8732 }
8733 }
8734
8735 /// SOUNDNESS R383 — the METHOD forms whose path is an ARGUMENT. The guard's `!c.method` gate
8736 /// excluded every method form because the path-STAT methods carry the path as their RECEIVER;
8737 /// `OpenOptions::open(path)` is the counter-example, and `allow Fs <lit>` exited 0 over a
8738 /// caller-supplied path because of it.
8739 #[test]
8740 fn fs_path_arg_methods_are_the_receiver_pairs_not_the_leaf() {
8741 // The path-taking pairs, matched on the TAIL so std/tokio/cap_primitives/fs_err all qualify.
8742 for p in ["std::fs::OpenOptions::open", "tokio::fs::OpenOptions::open",
8743 "cap_primitives::fs::OpenOptions::open", "fs_err::OpenOptions::open",
8744 "std::fs::DirBuilder::create", "tokio::fs::DirBuilder::create"] {
8745 assert!(is_fs_path_arg_method(p), "{p} takes its path as an ARGUMENT — R383");
8746 }
8747 // THE CONTROLS, and they are the row: every other method on the SAME receivers is a builder
8748 // modifier or a combinator, and `File`'s methods are use-verbs whose path was fixed at open.
8749 // Masking these is the over-mask a denylist would have caused (R379 measured that cost).
8750 for p in ["std::fs::File::read_to_string", "std::fs::File::write", "std::fs::File::write_all",
8751 "std::fs::File::read", "std::fs::File::seek", "std::path::Path::exists",
8752 "std::fs::OpenOptions::read", "std::fs::OpenOptions::truncate",
8753 "std::fs::OpenOptions::map_err", "tokio::fs::OpenOptions::mode"] {
8754 assert!(!is_fs_path_arg_method(p),
8755 "{p} hides no path — it is a builder modifier, a combinator, or a use-verb whose \
8756 path was fixed at open. Masking it is the over-mask this allowlist avoids.");
8757 }
8758 }
8759
8760 #[test]
8761 fn net_establishing_allowlist() {
8762 // Host-bearing verbs establish — a runtime value here is invisible, so the gate must fail closed.
8763 for m in ["connect", "connect_timeout", "get", "post", "put", "patch", "delete", "head",
8764 "request", "send_to", "lookup_host", "to_socket_addrs"] {
8765 assert!(is_net_establishing(m), "`{m}` takes the host as an argument and must establish");
8766 }
8767 // R379: the DNS resolver verbs `classify` already maps to Net. The WHOLE FAMILY, not the one
8768 // spelling that was found — `_lookup` is a suffix test mirroring `classify`'s own.
8769 for m in ["lookup", "lookup_ip", "resolve", "reverse_lookup", "srv_lookup", "mx_lookup",
8770 "txt_lookup", "ipv4_lookup", "ipv6_lookup", "ns_lookup", "soa_lookup"] {
8771 assert!(is_net_establishing(m),
8772 "`{m}` resolves a caller-supplied host — R379. Without it, `allow Net <benign \
8773 literal>` exits 0 over a DNS lookup of a runtime hostname.");
8774 }
8775 // USE-verbs on an already-established connection: the host was fixed at `connect`, so a missing
8776 // literal here hides nothing and must NOT flag — else `connect("h").write(..)` fails closed.
8777 for m in ["write", "read", "send", "flush", "recv", "peek", "write_all", "read_to_end",
8778 "write_vectored", "shutdown", "status", "headers", "text", "json"] {
8779 assert!(!is_net_establishing(m), "`{m}` is a use-verb on an established connection");
8780 }
8781 }
8782
8783 /// ⟨0.40⟩ SOUNDNESS R817 — an ACCEPT is not a BIND. The two lists answer different questions and must
8784 /// not share a member: a binding verb's literal is withheld and marks nothing, an accepting verb marks
8785 /// the surface incomplete. `accept`/`incoming` used to be binding verbs, which is the defect.
8786 #[test]
8787 fn r817_accepting_is_not_binding() {
8788 for (path, leaf) in [
8789 ("std::net::TcpListener::accept", "accept"),
8790 ("std::net::TcpListener::incoming", "incoming"),
8791 ("std::net::TcpListener::into_incoming", "into_incoming"),
8792 ("tokio::net::TcpListener::accept", "accept"),
8793 ("tokio::net::TcpListener::poll_accept", "poll_accept"),
8794 ("libc::accept", "accept"),
8795 ("libc::accept4", "accept4"),
8796 ("quinn::Endpoint::accept", "accept"),
8797 ("tonic::transport::server::Router::serve", "serve"),
8798 ("tonic::transport::server::Router::serve_with_shutdown", "serve_with_shutdown"),
8799 ("tonic::transport::server::TcpIncoming::new", "new"),
8800 ] {
8801 assert!(is_net_accepting(path, leaf), "`{path}` accepts connections: its peers are unseen");
8802 assert!(!is_net_binding(leaf), "`{leaf}` must not be a binding verb — binding withholds and marks nothing");
8803 }
8804 // CONTROLS — binds mark nothing; a stream accepted on an ESTABLISHED connection (quinn) has a peer
8805 // fixed at the endpoint's accept; a datagram receive is not an accept; `serve`/`new` elsewhere are
8806 // not accepts.
8807 for (path, leaf) in [
8808 ("std::net::UdpSocket::bind", "bind"),
8809 ("std::net::TcpListener::bind", "bind"),
8810 ("libc::listen", "listen"),
8811 ("quinn::Connection::accept_bi", "accept_bi"),
8812 ("quinn::Connection::accept_uni", "accept_uni"),
8813 ("std::net::UdpSocket::recv_from", "recv_from"),
8814 ("tonic::transport::server::Server::serve_with_shutdown", "serve_with_shutdown"),
8815 ("reqwest::Client::new", "new"),
8816 ] {
8817 assert!(!is_net_accepting(path, leaf), "`{path}` does not accept a connection");
8818 }
8819 for m in ["bind", "listen", "bind_to_device"] {
8820 assert!(is_net_binding(m), "`{m}` names a LOCAL address");
8821 }
8822 }
8823
8824 /// R379 — WHY THIS STAYS AN ALLOWLIST, which is NOT this family's usual direction.
8825 ///
8826 /// `candor-denylist-over-allowlist` says narrow a sound over-approximation with a denylist, and
8827 /// R350 made exactly that inversion one list over. It is wrong HERE, for the reason
8828 /// `is_cmd_naming_method` states two doc-blocks above for Exec: a WHOLE-CRATE-Net crate classifies
8829 /// every method as Net, so a denylist of use-verbs masks essentially everything in one. Measured by
8830 /// building the inversion and A/B-ing all 1,545 registry crates: **544 rows gained `incomplete`,
8831 /// none lost it** — `cap-std` 66, `hickory-net` 44 — and the triggers were conversions, accessors
8832 /// and combinators that hide no host.
8833 ///
8834 /// These are the measured leaks, pinned so the inversion cannot be re-attempted without meeting
8835 /// them. `bind` is the sharpest: this file has `is_net_binding` precisely because a bind address
8836 /// is where the process LISTENS, never a destination it reaches.
8837 #[test]
8838 fn the_rejected_denylist_leaks_are_not_establishing() {
8839 for m in ["bind", "into", "from", "from_std", "as_socket", "setsockopt", "getsockopt",
8840 "map_err", "map", "ok", "try_clone", "complete_io"] {
8841 assert!(!is_net_establishing(m),
8842 "`{m}` hides no host and must not mark a surface incomplete. It was a MEASURED \
8843 trigger of the rejected denylist inversion (544 rows gained `incomplete` over the \
8844 registry, 0 lost) — a whole-crate-Net crate classifies every method as Net, which \
8845 is why the allowlist is the precise side here. See R379.");
8846 }
8847 }
8848
8849 #[test]
8850 fn fs_path_arg_allowlist() {
8851 // The Fs masking guard's path-naming-fn allowlist — free fns / constructors take the path as a
8852 // string arg (a runtime path there is invisible to the gate). Stat methods (path on the receiver)
8853 // and handle ops carry no path arg and must NOT flag — but they're caught by the caller's
8854 // `!is_method` gate; the allowlist itself just enumerates the path-NAMING leaves.
8855 assert!(is_fs_path_arg("write") && is_fs_path_arg("read") && is_fs_path_arg("read_to_string"));
8856 assert!(is_fs_path_arg("open") && is_fs_path_arg("create") && is_fs_path_arg("create_new"));
8857 assert!(is_fs_path_arg("remove_file") && is_fs_path_arg("rename") && is_fs_path_arg("copy"));
8858 assert!(is_fs_path_arg("create_dir_all") && is_fs_path_arg("canonicalize") && is_fs_path_arg("metadata"));
8859 // handle ops / pure builders take NO path arg — never path-naming.
8860 assert!(!is_fs_path_arg("write_all") && !is_fs_path_arg("flush") && !is_fs_path_arg("read_exact"));
8861 assert!(!is_fs_path_arg("new") && !is_fs_path_arg("sync_all") && !is_fs_path_arg("set_len"));
8862 }
8863
8864 #[test]
8865 fn db_query_arg_allowlist() {
8866 // The Db masking guard's query-bearing-verb allowlist — these take the raw SQL as a string arg
8867 // (a runtime query there is invisible to the gate). Build-then-execute terminals and non-query
8868 // ops carry no SQL string and must NOT flag.
8869 assert!(is_db_query_arg("execute") && is_db_query_arg("query") && is_db_query_arg("query_one"));
8870 assert!(is_db_query_arg("prepare") && is_db_query_arg("batch_execute") && is_db_query_arg("execute_batch"));
8871 assert!(is_db_query_arg("query_row") && is_db_query_arg("query_map") && is_db_query_arg("exec"));
8872 // build-then-execute terminals (query built structurally, no SQL string) must NOT flag.
8873 assert!(!is_db_query_arg("fetch_all") && !is_db_query_arg("load") && !is_db_query_arg("first"));
8874 assert!(!is_db_query_arg("all") && !is_db_query_arg("one") && !is_db_query_arg("stream"));
8875 // connection / lifecycle ops take no SQL — must NOT flag.
8876 assert!(!is_db_query_arg("connect") && !is_db_query_arg("open") && !is_db_query_arg("begin"));
8877 assert!(!is_db_query_arg("commit") && !is_db_query_arg("ping") && !is_db_query_arg("get_conn"));
8878 }
8879}
8880
8881#[cfg(test)]
8882mod fs_kind_tests {
8883 use super::fs_kind;
8884
8885 /// SPEC §2 `fs`. Most of what these assert is what the classifier REFUSES to say: §2 requires the
8886 /// field be "omitted rather than guessed", because an empty or partial `fs` reads as a positive claim
8887 /// ("reads but never writes") — the §4 trust contract's forbidden direction.
8888 #[test]
8889 fn write_verbs() {
8890 for p in ["std::fs::write", "std::fs::create_dir_all", "std::fs::remove_file",
8891 "File::create", "std::fs::set_permissions", "f::write_all"] {
8892 assert_eq!(fs_kind(p), &["write"], "{p} mutates the disk");
8893 }
8894 }
8895
8896 #[test]
8897 fn read_verbs() {
8898 for p in ["std::fs::read_to_string", "std::fs::read_dir", "std::fs::metadata",
8899 "File::open", "std::fs::canonicalize", "f::read_to_end"] {
8900 assert_eq!(fs_kind(p), &["read"], "{p} observes without mutating");
8901 }
8902 }
8903
8904 /// A copy/rename reads the source AND writes the destination — one call, both kinds.
8905 #[test]
8906 fn two_locator_verbs_are_both() {
8907 for p in ["std::fs::copy", "std::fs::rename", "std::fs::hard_link"] {
8908 assert_eq!(fs_kind(p), &["read", "write"], "{p}");
8909 }
8910 }
8911
8912 /// THE LOAD-BEARING CASE. A verb that does not reveal direction must contribute NOTHING — not a
8913 /// default, not a guess. Anything here returning a kind would let a function claim "reads but never
8914 /// writes" on the strength of a verb that said neither.
8915 #[test]
8916 fn unrevealing_verbs_make_no_claim() {
8917 for p in ["std::fs::OpenOptions", "some_crate::do_thing", "std::fs::File", "f::seek"] {
8918 assert!(fs_kind(p).is_empty(), "{p} must not claim a direction it did not reveal");
8919 }
8920 }
8921
8922 /// `OpenOptions::open` is `File::open`'s leaf and NOT its verb: the direction lives in the builder
8923 /// chain (`.read(true)`/`.write(true)`), which `fs_kind` cannot see. A `write(true)` builder claiming
8924 /// `["read"]` is the forbidden direction, so the type wins over the leaf here. `File::open` — where
8925 /// `open` DOES mean read — is unaffected, which is the whole reason this is keyed on the type.
8926 #[test]
8927 fn openoptions_open_claims_no_direction_but_file_open_still_reads() {
8928 for p in ["std::fs::OpenOptions::open", "fs_err::OpenOptions::open",
8929 "tokio::fs::OpenOptions::open"] {
8930 assert!(fs_kind(p).is_empty(),
8931 "{p}'s direction was set by the builder chain, which this function cannot see");
8932 }
8933 assert_eq!(fs_kind("std::fs::File::open"), &["read"], "`File::open` is unambiguously a read");
8934 }
8935}
8936
8937#[cfg(test)]
8938mod masks_locator_tests {
8939 use super::*;
8940
8941 /// SOUNDNESS R806 — each no-locator DENYLIST must be a SUBSET of the table it subtracts from. A
8942 /// typo'd or stale entry that is not in the table would subtract nothing and read as a decision;
8943 /// a table entry renamed without its denylist twin would silently start masking (the safe
8944 /// direction) — but only this check says which happened.
8945 #[test]
8946 fn every_no_locator_denylist_is_a_subset_of_its_charging_table() {
8947 for n in LIBC_FS_NO_PATH {
8948 assert!(LIBC_FS.contains(n), "LIBC_FS_NO_PATH names `{n}`, which LIBC_FS does not charge");
8949 }
8950 for n in LIBC_EXEC_NO_PROGRAM {
8951 assert!(LIBC_EXEC.contains(n), "LIBC_EXEC_NO_PROGRAM names `{n}`, absent from LIBC_EXEC");
8952 }
8953 for n in LIBC_NET_NO_DESTINATION {
8954 assert!(LIBC_NET.contains(n), "LIBC_NET_NO_DESTINATION names `{n}`, absent from LIBC_NET");
8955 }
8956 for n in PG_DB_VERBS_NO_SQL {
8957 assert!(PG_DB_VERBS.contains(n), "PG_DB_VERBS_NO_SQL names `{n}`, absent from PG_DB_VERBS");
8958 }
8959 }
8960
8961 /// SOUNDNESS R806 — THE DERIVATION, pinned from the table side: every leaf the table charges that is
8962 /// not on the denylist MASKS. So a leaf added to `LIBC_FS` tomorrow is masked with no second edit,
8963 /// which is the property the hand list lacked.
8964 #[test]
8965 fn every_charged_libc_leaf_off_the_denylist_masks() {
8966 for (eff, table, deny) in [
8967 ("Fs", LIBC_FS, LIBC_FS_NO_PATH),
8968 ("Exec", LIBC_EXEC, LIBC_EXEC_NO_PROGRAM),
8969 ("Net", LIBC_NET, LIBC_NET_NO_DESTINATION),
8970 ] {
8971 for leaf in table.iter().filter(|l| !deny.contains(l)) {
8972 for cr in ["libc", "nix"] {
8973 let path = format!("{cr}::{leaf}");
8974 assert_eq!(classify(cr, &path), Some(eff), "{path} must still be charged {eff}");
8975 assert!(masks_locator(eff, cr, &path, leaf, false), "{path} names a locator — must mask");
8976 }
8977 }
8978 for leaf in deny.iter() {
8979 let path = format!("libc::{leaf}");
8980 assert!(!masks_locator(eff, "libc", &path, leaf, false) || is_net_establishing(leaf),
8981 "{path} is on the no-locator denylist — must not mask via the table");
8982 }
8983 }
8984 // The named members, from the row: the sweep's EXECUTED arms.
8985 for (eff, leaf) in [("Fs", "unlink"), ("Fs", "unlinkat"), ("Fs", "utimensat"), ("Exec", "system"),
8986 ("Exec", "popen"), ("Exec", "execvp"), ("Exec", "posix_spawn"), ("Net", "getaddrinfo"),
8987 ("Net", "sendto")] {
8988 assert!(masks_locator(eff, "libc", &format!("libc::{leaf}"), leaf, false), "libc::{leaf}");
8989 }
8990 // Scoped to the syscall crates: the same leaf on an unrelated crate is not this table's to judge.
8991 assert!(!masks_locator("Fs", "mycrate", "mycrate::unlink", "unlink", false));
8992 // rustix spells `sendmsg` WITHOUT an address; libc and nix carry one.
8993 assert!(!masks_locator("Net", "rustix", "rustix::net::sendmsg", "sendmsg", false));
8994 assert!(masks_locator("Net", "libc", "libc::sendmsg", "sendmsg", false));
8995 }
8996
8997 #[test]
8998 fn pg_verbs_take_sql_except_the_named_three() {
8999 for v in PG_DB_VERBS.iter().filter(|v| !PG_DB_VERBS_NO_SQL.contains(v)) {
9000 let path = format!("deadpool_postgres::Client{v}");
9001 let leaf = &v[2..];
9002 assert!(masks_locator("Db", "deadpool_postgres", &path, leaf, true), "{path} takes SQL — must mask");
9003 }
9004 assert!(masks_locator("Db", "tokio_postgres", "tokio_postgres::Client::prepare_typed_cached",
9005 "prepare_typed_cached", true));
9006 assert!(!masks_locator("Db", "tokio_postgres", "tokio_postgres::Client::transaction", "transaction", true));
9007 assert!(masks_locator("Db", "sqlx_core", "sqlx_core::executor::Executor::prepare_with", "prepare_with", true));
9008 assert!(!masks_locator("Db", "sqlx_core", "sqlx_core::query::Query::fetch_all", "fetch_all", true),
9009 "a Query terminal takes an EXECUTOR at arg 0 — masking it would swallow every literal query");
9010 }
9011
9012 /// THE COVERAGE-GATE REFRESH (2026-10-08) — each family's rule fires on the spellings a consumer writes
9013 /// and NOT on a sibling type or an accessor (each charge is traced to a body in the rule's comment).
9014 #[test]
9015 fn coverage_gate_refresh_2026_10_08_rules() {
9016 for p in ["rusqlite::Transaction::commit", "rusqlite::transaction::Transaction::new",
9017 "rusqlite::Savepoint::rollback", "rusqlite::Connection::pragma_update",
9018 "rusqlite::Connection::unchecked_transaction", "rusqlite::Connection::savepoint_with_name"] {
9019 assert_eq!(classify("rusqlite", p), Some("Db"), "{p}");
9020 }
9021 for p in ["rusqlite::Transaction::drop_behavior", "rusqlite::Savepoint::set_drop_behavior"] {
9022 assert_eq!(classify("rusqlite", p), None, "{p}");
9023 }
9024 for p in ["redis::Connection::send_packed_command", "redis::connection::Connection::recv_response",
9025 "redis::PubSub::get_message"] {
9026 assert_eq!(classify("redis", p), Some("Net"), "{p}");
9027 }
9028 assert_eq!(classify("redis", "redis::aio::MultiplexedConnection::recv_response"), None);
9029 assert_eq!(classify("redis", "redis::Connection::set_write_timeout"), None);
9030 for p in ["lettre::SmtpConnection::command", "lettre::transport::smtp::client::SmtpConnection::connect",
9031 "lettre::AsyncSmtpConnection::read_response",
9032 "lettre::transport::smtp::client::AsyncNetworkStream::connect_tokio1",
9033 "lettre::Tokio1Executor::connect"] {
9034 assert_eq!(classify("lettre", p), Some("Net"), "{p}");
9035 }
9036 for p in ["lettre::SmtpConnection::server_info", "lettre::NetworkStream::peer_addr",
9037 "lettre::AsyncSmtpConnection::is_encrypted"] {
9038 assert_eq!(classify("lettre", p), None, "{p}");
9039 }
9040 assert_eq!(classify("rustls", "rustls::KeyLogFile::log"), Some("Fs"));
9041 assert_eq!(classify("rustls", "rustls::NoKeyLog::log"), None);
9042 }
9043
9044 /// SOUNDNESS R1528 (b11) — the last 51 grandfathered rows: each rule beside a same-type pure neighbour (a rule that charged the
9045 /// whole type would pass without them) and the two carve-outs that REMOVE a charge. Every positive is EXECUTED in the handoff
9046 /// `rust-r1528/b11/`; the controls below are the idle windows of the same fixture.
9047 #[test]
9048 fn r1528_b11_rows_classify_with_pure_neighbours() {
9049 let eff = |c: &str, p: &str| classify(c, p);
9050 // env_logger: the five timestamp readers and the default format
9051 for m in ["timestamp", "timestamp_seconds", "timestamp_millis", "timestamp_micros", "timestamp_nanos"] {
9052 assert_eq!(eff("env_logger", &format!("env_logger::fmt::humantime::Formatter::{m}")), Some("Clock"));
9053 assert_eq!(eff("env_logger", &format!("env_logger::fmt::Formatter::{m}")), Some("Clock"));
9054 }
9055 assert_eq!(eff("env_logger", "env_logger::fmt::ConfigurableFormat::format"), Some("Clock"));
9056 assert_eq!(eff("env_logger", "env_logger::fmt::ConfigurableFormat::timestamp"), None); // the setter
9057 assert_eq!(eff("env_logger", "env_logger::fmt::Formatter::style"), None);
9058 // elasticsearch: the transport constructors (+ the trust store as the SECOND effect) and the pool reseed
9059 for m in ["single_node", "static_node_list", "sniffing_node_list", "cloud"] {
9060 let p = format!("elasticsearch::http::transport::Transport::{m}");
9061 assert_eq!(eff("elasticsearch", &p), Some("Env"), "{p}");
9062 assert_eq!(also_effects("elasticsearch", &p), vec!["Fs"], "{p} must carry the trust-store read");
9063 }
9064 assert_eq!(eff("elasticsearch", "elasticsearch::http::transport::TransportBuilder::build"), Some("Env"));
9065 assert_eq!(also_effects("elasticsearch", "elasticsearch::http::transport::TransportBuilder::build"), vec!["Fs"]);
9066 assert_eq!(eff("elasticsearch", "elasticsearch::http::transport::MultiNodeConnectionPool::reseed"), Some("Clock"));
9067 assert_eq!(eff("elasticsearch", "elasticsearch::http::transport::TransportBuilder::auth"), None);
9068 assert!(also_effects("elasticsearch", "elasticsearch::http::transport::TransportBuilder::auth").is_empty());
9069 assert!(also_effects("elasticsearch", "elasticsearch::http::transport::Transport::send").is_empty());
9070 assert!(also_effects("reqwest", "reqwest::ClientBuilder::build").is_empty(), "reqwest is not claimed here");
9071 // tokio_postgres / deadpool_postgres: the verbs `PG_DB_VERBS` never matched
9072 for m in ["execute_raw", "execute_typed", "query_typed_raw", "query_typed_one", "query_typed_opt", "query_scalar",
9073 "query_one_scalar", "query_opt_scalar", "bind", "bind_raw", "query_portal", "query_portal_raw",
9074 "commit", "rollback", "savepoint"] {
9075 for ty in ["Client", "Transaction"] {
9076 assert_eq!(eff("tokio_postgres", &format!("tokio_postgres::{ty}::{m}")), Some("Db"), "{ty}::{m}");
9077 assert_eq!(eff("deadpool_postgres", &format!("deadpool_postgres::{ty}::{m}")), Some("Db"), "dp {ty}::{m}");
9078 }
9079 }
9080 for m in ["simple_query_raw", "check_connection"] {
9081 assert_eq!(eff("tokio_postgres", &format!("tokio_postgres::client::Client::{m}")), Some("Db"));
9082 }
9083 assert_eq!(eff("deadpool_postgres", "deadpool_postgres::GenericClient::execute_raw"), Some("Db"));
9084 assert_eq!(eff("tokio_postgres", "tokio_postgres::to_statement::private::ToStatementType::into_statement"), Some("Db"));
9085 for p in ["tokio_postgres::Client::is_closed", "tokio_postgres::Client::cancel_token", "tokio_postgres::Client::clear_type_cache",
9086 "tokio_postgres::Transaction::client", "tokio_postgres::Connection::poll_message"] {
9087 assert_eq!(eff("tokio_postgres", p), None, "{p} sends nothing (poll_message is filed R1598:conditional)");
9088 }
9089 assert_eq!(eff("deadpool_postgres", "deadpool_postgres::config::Config::get_pg_config"), None); // R1598:conditional
9090 // grep_cli: associate reads PATH; the MATCHER builder's build spawns nothing, the READER builders' do
9091 for m in ["associate", "try_associate"] {
9092 assert_eq!(eff("grep_cli", &format!("grep_cli::decompress::DecompressionMatcherBuilder::{m}")), Some("Env"));
9093 }
9094 assert_eq!(eff("grep_cli", "grep_cli::decompress::DecompressionMatcherBuilder::build"), None, "EXECUTED 0 spawns");
9095 assert_eq!(eff("grep_cli", "grep_cli::decompress::DecompressionReaderBuilder::build"), Some("Exec"));
9096 assert_eq!(eff("grep_cli", "grep_cli::process::CommandReaderBuilder::build"), Some("Exec"));
9097 assert_eq!(eff("grep_cli", "grep_cli::decompress::DecompressionMatcherBuilder::new"), None);
9098 // backoff
9099 for m in ["get_elapsed_time", "reset", "default"] {
9100 assert_eq!(eff("backoff", &format!("backoff::exponential::ExponentialBackoff::{m}")), Some("Clock"), "{m}");
9101 }
9102 assert_eq!(eff("backoff", "backoff::exponential::ExponentialBackoff::next_backoff"), Some("Rand"));
9103 assert_eq!(eff("backoff", "backoff::exponential::ExponentialBackoffBuilder::build"), Some("Clock"));
9104 assert_eq!(eff("backoff", "backoff::exponential::ExponentialBackoffBuilder::new"), None);
9105 assert_eq!(eff("backoff", "backoff::exponential::ExponentialBackoffBuilder::with_max_elapsed_time"), None);
9106 assert_eq!(eff("backoff", "backoff::clock::SystemClock::now"), Some("Clock"));
9107 // rustls: the client connection draws entropy; the server connection's constructor draws none
9108 for p in ["rustls::client::client_conn::connection::ClientConnection::new", "rustls::ClientConnection::new_with_alpn",
9109 "rustls::client::client_conn::UnbufferedClientConnection::new", "rustls::client::UnbufferedClientConnection::new_with_alpn",
9110 "rustls::quic::connection::ClientConnection::new", "rustls::quic::connection::ClientConnection::new_with_alpn"] {
9111 assert_eq!(eff("rustls", p), Some("Rand"), "{p}");
9112 }
9113 assert_eq!(eff("rustls", "rustls::ServerConnection::new"), None);
9114 assert_eq!(eff("rustls", "rustls::ClientConfig::builder"), None);
9115 assert_eq!(eff("rustls", "rustls::KeyLogFile::new"), Some("Fs"));
9116 // etcetera
9117 for p in ["etcetera::base_strategy::apple::Apple::new", "etcetera::base_strategy::xdg::Xdg::new", "etcetera::base_strategy::windows::Windows::new",
9118 "etcetera::app_strategy::unix::Unix::new", "etcetera::app_strategy::xdg::Xdg::new", "etcetera::app_strategy::apple::Apple::new"] {
9119 assert_eq!(eff("etcetera", p), Some("Env"), "{p}");
9120 }
9121 assert_eq!(eff("etcetera", "etcetera::app_strategy::AppStrategyArgs::new"), None);
9122 // time
9123 assert_eq!(eff("time", "time::instant::Instant::elapsed"), Some("Clock"));
9124 assert_eq!(eff("time", "time::signed_duration::SignedDuration::time_fn"), Some("Clock"));
9125 assert_eq!(eff("time", "time::Duration::time_fn"), Some("Clock"));
9126 assert_eq!(eff("time", "time::utc_offset::UtcOffset::current_local_offset"), Some("Clock"));
9127 assert_eq!(eff("time", "time::Instant::checked_add"), None);
9128 assert_eq!(eff("time", "time::UtcOffset::from_hms"), None);
9129 // console (the free fns are spelled at the crate root and under `utils`/`term`)
9130 for p in ["console::is_dumb", "console::term::is_dumb", "console::set_colors_enabled", "console::utils::set_colors_enabled_stderr",
9131 "console::set_true_colors_enabled", "console::true_colors_enabled_stderr", "console::colors_enabled"] {
9132 assert_eq!(eff("console", p), Some("Env"), "{p}");
9133 }
9134 assert_eq!(eff("console", "console::style"), None);
9135 assert_eq!(eff("console", "console::measure_text_width"), None);
9136 // rand
9137 assert_eq!(eff("rand", "rand::make_rng"), Some("Rand"));
9138 assert_eq!(eff("rand", "rand::SeedableRng::seed_from_u64"), None);
9139 assert_eq!(eff("rand", "rand::rng::u8::fill_slice"), None); // R1598:conditional
9140 // portable_pty: the builder snapshots the environment and spawns nothing — `from_argv`/`new_default_prog` were `Exec`
9141 for m in ["new", "from_argv", "new_default_prog"] {
9142 assert_eq!(eff("portable_pty", &format!("portable_pty::cmdbuilder::CommandBuilder::{m}")), Some("Env"), "{m}");
9143 }
9144 assert_eq!(eff("portable_pty", "portable_pty::CommandBuilder::spawn_command"), Some("Exec"));
9145 assert_eq!(eff("portable_pty", "portable_pty::CommandBuilder::arg"), None);
9146 assert_eq!(eff("portable_pty", "portable_pty::CommandBuilder::get_shell"), Some("Fs"));
9147 // notify / native_tls / duct / dotenvy
9148 assert_eq!(eff("notify", "notify::poll::PollWatcher::with_initial_scan"), Some("Clock"));
9149 assert_eq!(eff("notify", "notify::poll::PollWatcher::poll"), None);
9150 assert_eq!(eff("native_tls_crate", "native_tls_crate::TlsConnectorBuilder::build"), Some("Fs"));
9151 assert_eq!(eff("native_tls_crate", "native_tls_crate::TlsConnectorBuilder::min_protocol_version"), None);
9152 assert_eq!(eff("duct", "duct::Handle::wait_timeout"), Some("Clock"));
9153 assert_eq!(eff("duct", "duct::Handle::wait_deadline"), Some("Clock"));
9154 assert_eq!(eff("duct", "duct::Handle::try_wait"), None);
9155 assert_eq!(eff("duct", "duct::Handle::wait"), None);
9156 assert_eq!(eff("dotenvy", "dotenvy::iter::Iter::load_override"), Some("Env"));
9157 assert_eq!(eff("dotenvy", "dotenvy::iter::Iter::load"), Some("Env"));
9158 }
9159
9160 /// SOUNDNESS R1528 (b10) — tempfile 3.27.0, tungstenite 0.30.0, crossterm 0.29.0, reqwest 0.13.5, mysql 28.0.3 and diesel
9161 /// 2.3.14: every rule against a same-type pure neighbour, in the spelling the scan produces (a chain hop is attributed to
9162 /// the root type) and in the declaring-module spelling.
9163 #[test]
9164 fn r1528_b10_rows_classify_with_pure_neighbours() {
9165 // tempfile
9166 for p in ["tempfile::SpooledTempFile::into_file", "tempfile::spooled::SpooledTempFile::roll"] {
9167 assert_eq!(classify("tempfile", p), Some("Fs"), "{p}");
9168 }
9169 assert_eq!(classify("tempfile", "tempfile::env::temp_dir"), Some("Env"));
9170 assert_eq!(classify("tempfile", "tempfile::temp_dir"), Some("Env"));
9171 for p in ["tempfile::SpooledTempFile::new", "tempfile::SpooledTempFile::flush", "tempfile::TempDir::path",
9172 "tempfile::env::override_temp_dir", "tempfile::TempPath::from_path", "tempfile::TempPath::try_from_path"] {
9173 assert_eq!(classify("tempfile", p), None, "{p}");
9174 }
9175 // tungstenite: the pass-through `Request` impl must stay pure (a `::into_client_request` suffix would charge it)
9176 assert_eq!(classify("tungstenite", "tungstenite::handshake::client::generate_key"), Some("Rand"));
9177 for p in ["tungstenite::http::Uri::into_client_request", "tungstenite::client::ClientRequestBuilder::into_client_request"] {
9178 assert_eq!(classify("tungstenite", p), Some("Rand"), "{p}");
9179 }
9180 assert_eq!(classify("tungstenite", "tungstenite::http::Request::into_client_request"), None);
9181 assert_eq!(classify("tungstenite", "tungstenite::client::ClientRequestBuilder::new"), None);
9182 assert_eq!(classify("tungstenite", "tungstenite::client::ClientRequestBuilder::with_header"), None);
9183 for p in ["tungstenite::stream::MaybeTlsStream::set_nodelay", "tungstenite::stream::NoDelay::set_nodelay",
9184 "tungstenite::WebSocket::write_pending", "tungstenite::protocol::WebSocket::write_pending"] {
9185 assert_eq!(classify("tungstenite", p), Some("Net"), "{p}");
9186 }
9187 assert_eq!(classify("tungstenite", "tungstenite::Message::text"), None);
9188 // crossterm
9189 for p in ["crossterm::style::Colored::ansi_color_disabled", "crossterm::style::Colored::ansi_color_disabled_memoized",
9190 "crossterm::Colored::set_ansi_color_disabled", "crossterm::style::force_color_output",
9191 "crossterm::style::available_color_count"] {
9192 assert_eq!(classify("crossterm", p), Some("Env"), "{p}");
9193 }
9194 for p in ["crossterm::cursor::position", "crossterm::cursor::sys::unix::position", "crossterm::event::EventStream::new",
9195 "crossterm::event::EventStream::next"] {
9196 assert_eq!(classify("crossterm", p), Some("Ipc"), "{p}");
9197 }
9198 // the conditional rows and the pure neighbours stay unclaimed
9199 for p in ["crossterm::style::Print::write_ansi", "crossterm::terminal::SetTitle::write_ansi",
9200 "crossterm::terminal::SetTitle::execute_winapi", "crossterm::ansi_support::supports_ansi",
9201 "crossterm::style::Colored::ForegroundColor", "crossterm::style::Print", "crossterm::style::Color::Red"] {
9202 assert_eq!(classify("crossterm", p), None, "{p}");
9203 }
9204 // reqwest
9205 for p in ["reqwest::ClientBuilder::build", "reqwest::blocking::ClientBuilder::build", "reqwest::Client::build",
9206 "reqwest::blocking::Client::build", "reqwest::Client::new", "reqwest::NoProxy::from_env"] {
9207 assert_eq!(classify("reqwest", p), Some("Env"), "{p}");
9208 }
9209 for p in ["reqwest::multipart::Form::file", "reqwest::blocking::multipart::Form::file"] {
9210 assert_eq!(classify("reqwest", p), Some("Fs"), "{p}");
9211 }
9212 assert_eq!(classify("reqwest", "reqwest::blocking::Response::copy_to"), Some("Net"));
9213 for p in ["reqwest::ClientBuilder::new", "reqwest::Client::builder", "reqwest::multipart::Form::new",
9214 "reqwest::multipart::Form::text", "reqwest::NoProxy::from_string",
9215 "reqwest::async_impl::h3_client::pool::PoolClient::send_request"] {
9216 assert_eq!(classify("reqwest", p), None, "{p}");
9217 }
9218 // mysql
9219 for p in ["mysql::Conn::reset", "mysql::Conn::change_user", "mysql::PooledConn::change_user", "mysql::Pool::new",
9220 "mysql::Pool::try_get_conn", "mysql::Pool::start_transaction", "mysql::PooledConn::get_binlog_stream",
9221 "mysql::Conn::start_transaction", "mysql::PooledConn::start_transaction", "mysql::Conn::get_binlog_stream",
9222 "mysql::conn::pool::Pool::new"] {
9223 assert_eq!(classify("mysql", p), Some("Db"), "{p}");
9224 }
9225 for p in ["mysql::PooledConn::unwrap", "mysql::Pool::opts", "mysql::PoolConstraints::new", "mysql::Opts::from_url",
9226 "mysql::TxOpts::default", "mysql::BinlogRequest::new"] {
9227 assert_eq!(classify("mysql", p), None, "{p}");
9228 }
9229 // diesel
9230 for p in ["diesel::sqlite::SqliteConnection::batch_execute", "diesel::SqliteConnection::immediate_transaction",
9231 "diesel::sqlite::SqliteConnection::exclusive_transaction", "diesel::connection::AnsiTransactionManager::begin_transaction_sql",
9232 "diesel::sqlite::SqliteConnection::serialize_database_to_buffer",
9233 "diesel::sqlite::SqliteConnection::deserialize_readonly_database_from_buffer", "diesel::pg::TransactionBuilder::run",
9234 "diesel::pg::PgConnection::run", "diesel::pg::PgConnection::transaction", "diesel::sqlite::SqliteConnection::transaction"] {
9235 assert_eq!(classify("diesel", p), Some("Db"), "{p}");
9236 }
9237 for p in ["diesel::pg::PgConnection::build_transaction", "diesel::connection::AnsiTransactionManager::new",
9238 "diesel::sqlite::SqliteConnection::set_instrumentation"] {
9239 assert_eq!(classify("diesel", p), None, "{p}");
9240 }
9241 }
9242
9243 /// SOUNDNESS R1597 — lettre `send`/`send_raw` charge the TRANSPORT's effect. SMTP and the generic trait path stay `Net`.
9244 #[test]
9245 fn r1597_lettre_send_charges_the_transports_own_effect() {
9246 for p in ["lettre::FileTransport::send", "lettre::FileTransport::send_raw", "lettre::AsyncFileTransport::send",
9247 "lettre::AsyncFileTransport::send_raw", "lettre::transport::file::FileTransport::send",
9248 "lettre::transport::file::AsyncFileTransport::send"] {
9249 assert_eq!(classify("lettre", p), Some("Fs"), "{p}");
9250 }
9251 for p in ["lettre::SendmailTransport::send", "lettre::AsyncSendmailTransport::send",
9252 "lettre::transport::sendmail::SendmailTransport::send_raw"] {
9253 assert_eq!(classify("lettre", p), Some("Exec"), "{p}");
9254 }
9255 for p in ["lettre::SmtpTransport::send", "lettre::AsyncSmtpTransport::send", "lettre::transport::smtp::SmtpTransport::send_raw",
9256 "lettre::Transport::send", "lettre::AsyncTransport::send"] {
9257 assert_eq!(classify("lettre", p), Some("Net"), "{p}");
9258 }
9259 // the sibling the file transport already had: `read` is `Fs` (unchanged)
9260 assert_eq!(classify("lettre", "lettre::FileTransport::read"), Some("Fs"));
9261 }
9262
9263 /// SOUNDNESS R1528 — rules for rows EXECUTED/verified true, each with a same-crate pure neighbour.
9264 #[test]
9265 fn r1528_verified_rows_classify() {
9266 // tempfile: `into_file` drops the TempPath (unlinks); the `with_*` dir constructors create.
9267 assert_eq!(classify("tempfile", "tempfile::NamedTempFile::into_file"), Some("Fs"));
9268 for v in ["with_prefix", "with_prefix_in", "with_suffix", "with_suffix_in"] {
9269 assert_eq!(classify("tempfile", &format!("tempfile::TempDir::{v}")), Some("Fs"), "{v}");
9270 }
9271 assert_eq!(classify("tempfile", "tempfile::TempDir::into_path"), None, "flag flip only (R1528 FALSE)");
9272 // R1528 (b10): `TempDir::keep` is the same flag flip, EXECUTED 0 events; it was charged `Fs` by the `::keep` suffix rule.
9273 assert_eq!(classify("tempfile", "tempfile::TempDir::keep"), None, "flag flip only, carved out of ::keep");
9274 assert_eq!(classify("tempfile", "tempfile::NamedTempFile::keep"), Some("Fs"), "NamedTempFile::keep is not carved out");
9275 assert_eq!(classify("tempfile", "tempfile::TempPath::keep"), Some("Fs"), "TempPath::keep is not carved out");
9276 assert_eq!(classify("tempfile", "tempfile::NamedTempFile::path"), None);
9277 // R1528 (b10): `SpooledTempFile::into_file` creates a temp file for an in-memory buffer (EXECUTED), so it IS claimed now.
9278 assert_eq!(classify("tempfile", "tempfile::SpooledTempFile::into_file"), Some("Fs"));
9279 // chrono: the deprecated `today` is `now().date()`; no other `today` is claimed.
9280 assert_eq!(classify("chrono", "chrono::Utc::today"), Some("Clock"));
9281 assert_eq!(classify("chrono", "chrono::Local::today"), Some("Clock"));
9282 assert_eq!(classify("chrono", "chrono::offset::utc::Utc::today"), Some("Clock"));
9283 assert_eq!(classify("chrono", "chrono::NaiveDate::today"), None);
9284 assert_eq!(classify("chrono", "chrono::DateTime::parse_from_rfc2822"), None, "oracle fabrication, R1420(a)");
9285 // tokio_postgres cancel_query forwarders open a fresh connection; postgres is_valid is a round-trip.
9286 assert_eq!(classify("tokio_postgres", "tokio_postgres::Client::cancel_query"), Some("Net"));
9287 assert_eq!(classify("tokio_postgres", "tokio_postgres::Transaction::cancel_query"), Some("Net"));
9288 assert_eq!(classify("tokio_postgres", "tokio_postgres::Client::is_closed"), None);
9289 assert_eq!(classify("postgres", "postgres::Client::is_valid"), Some("Db"));
9290 assert_eq!(classify("tokio_postgres", "tokio_postgres::Client::is_valid"), None);
9291 // rdkafka: a broker request, every spelling.
9292 for p in ["rdkafka::Client::fetch_group_list", "rdkafka::BaseConsumer::fetch_group_list",
9293 "rdkafka::StreamConsumer::fetch_group_list", "rdkafka::consumer::Consumer::fetch_group_list"] {
9294 assert_eq!(classify("rdkafka", p), Some("Net"), "{p}");
9295 }
9296 // tracing_subscriber: `fmt::Layer::new` reads NO_COLOR; the root `Layer` trait has no `new`.
9297 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::Layer::new"), Some("Env"));
9298 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::fmt_layer::Layer::new"), Some("Env"));
9299 assert_eq!(classify("tracing_subscriber", "tracing_subscriber::fmt::Layer::with_writer"), None);
9300 }
9301
9302 /// SOUNDNESS R1528 — chrono 0.4.45, tracing-subscriber 0.3.23 and jiff 0.2.38 rows that carry a rule, each
9303 /// EXECUTED against a consumer fixture (handoff `rust-r1528/clockenv`). Pure neighbours and the rows filed as
9304 /// conditional (R1557/R1558/R1559) are pinned to `None` so a later "tidy-up" cannot over-charge them.
9305 #[test]
9306 fn r1528_clock_env_rows_classify() {
9307 for p in ["Local::offset_from_utc_datetime", "offset::local::Local::offset_from_local_datetime",
9308 "Local::offset_from_utc_date", "offset::Local::offset_from_local_date"] {
9309 assert_eq!(classify("chrono", &format!("chrono::{p}")), Some("Clock"), "{p}");
9310 }
9311 for p in ["TimeZone::offset_from_utc_datetime", "FixedOffset::offset_from_utc_datetime",
9312 "Utc::offset_from_utc_datetime", "DateTime::with_timezone", "Date::with_timezone",
9313 "DateTime::checked_add_months", "DateTime::checked_sub_months", "DateTime::duration_round",
9314 "DateTime::duration_round_up", "DateTime::duration_trunc", "NaiveDateTime::and_local_timezone",
9315 "Parsed::to_datetime_with_timezone", "DateTime::fixed_offset", "DateTime::years_since",
9316 "DateTime::parse_from_rfc3339", "DateTime::parse_from_rfc2822", "DateTime::parse_from_str",
9317 "DateTime::parse_and_remainder", "Parsed::to_datetime", "NaiveDate::today"] {
9318 assert_eq!(classify("chrono", &format!("chrono::{p}")), None, "{p}");
9319 }
9320 assert_eq!(classify("mycrate", "mycrate::Local::offset_from_utc_datetime"), None);
9321 // SOUNDNESS R1616 — TimeZone's provided members on Local reach the same lookups; on Utc/FixedOffset they do not.
9322 for m in ["from_utc_datetime", "from_local_datetime", "timestamp_opt", "with_ymd_and_hms", "ymd_opt", "datetime_from_str"] {
9323 assert_eq!(classify("chrono", &format!("chrono::Local::{m}")), Some("Clock"), "{m}");
9324 assert_eq!(classify("chrono", &format!("chrono::offset::local::Local::{m}")), Some("Clock"), "{m}");
9325 assert_eq!(classify("chrono", &format!("chrono::Utc::{m}")), None, "{m}");
9326 assert_eq!(classify("chrono", &format!("chrono::FixedOffset::{m}")), None, "{m}");
9327 }
9328
9329 for p in ["SystemTime::format_time", "fmt::time::SystemTime::format_time", "UtcTime::format_time",
9330 "fmt::time::time_crate::LocalTime::format_time", "OffsetTime::format_time",
9331 "ChronoLocal::format_time", "fmt::time::chrono_crate::ChronoUtc::format_time", "Uptime::default",
9332 "fmt::time::Uptime::default", "uptime", "fmt::time::uptime"] {
9333 assert_eq!(classify("tracing_subscriber", &format!("tracing_subscriber::{p}")), Some("Clock"), "{p}");
9334 }
9335 for p in ["fmt::layer", "fmt::fmt", "fmt", "Layer::default", "fmt::Layer::default", "fmt::fmt_layer::Layer::default",
9336 "fmt::Subscriber::builder", "FmtSubscriber::builder", "fmt::Subscriber::default",
9337 "fmt::SubscriberBuilder::default"] {
9338 let want = if p == "Layer::default" { None } else { Some("Env") };
9339 assert_eq!(classify("tracing_subscriber", &format!("tracing_subscriber::{p}")), want, "{p}");
9340 }
9341 // forwarders (R1558) and pure neighbours stay unclaimed: `Layer::on_event` is ALSO the trait method every
9342 // layer shares, so a rule on it would charge every pure layer.
9343 for p in ["Layer::on_event", "Layer::on_new_span", "Layer::on_enter", "Layer::on_exit", "Layer::on_close",
9344 "Layered::on_event", "Filtered::on_event", "Option::on_event", "Context::event",
9345 "Format::format_event", "fmt::format::json::Format::format_event", "Uptime::format_time",
9346 "Uptime::from", "time", "fmt::time::time", "SubscriberBuilder::with_target",
9347 "fmt::SubscriberBuilder::with_writer"] {
9348 assert_eq!(classify("tracing_subscriber", &format!("tracing_subscriber::{p}")), None, "{p}");
9349 }
9350
9351 for p in ["Date::in_tz", "civil::DateTime::in_tz", "Timestamp::in_tz", "zoned::Zoned::in_tz",
9352 "TimeZoneDatabase::from_env", "tz::db::TimeZoneDatabase::from_dir",
9353 "TimeZoneDatabase::from_concatenated_path", "DateTimeParser::parse_zoned",
9354 "fmt::temporal::DateTimeParser::parse_time_zone", "BrokenDownTime::to_zoned",
9355 "fmt::strtime::BrokenDownTime::to_zoned", "Pieces::to_time_zone",
9356 "fmt::temporal::pieces::TimeZoneAnnotation::to_time_zone", "db", "tz::db", "tz::db::db"] {
9357 assert_eq!(classify("jiff", &format!("jiff::{p}")), Some("Fs"), "{p}");
9358 }
9359 for p in ["BrokenDownTime::to_zoned_with", "Pieces::to_time_zone_with", "TimeZoneAnnotation::to_time_zone_with",
9360 "TimeZoneDatabase::bundled", "DateTimeParser::parse_zoned_with", "Date::to_zoned", "TimeZone::fixed",
9361 "Span::checked_add", "Timestamp::from_second"] {
9362 assert_eq!(classify("jiff", &format!("jiff::{p}")), None, "{p}");
9363 }
9364 assert_eq!(classify("mycrate", "mycrate::Date::in_tz"), None);
9365 }
9366
9367 /// SOUNDNESS R1528 — the sea-orm 2.0.4 rows that carry a rule, each EXECUTED against a consumer fixture on a
9368 /// file-backed sqlite database (handoff `rust-r1528/seaorm`). The pure neighbours, the `RusqliteSharedConnection`
9369 /// and `Vec::load_*` rows (filed as R1570/R1571: no path ever reaches `classify`) and the mock/proxy lookalikes are
9370 /// pinned to `None` so a later "tidy-up" cannot turn them into fabrications or dead coverage.
9371 #[test]
9372 fn r1528_sea_orm_rows_classify() {
9373 let db = |ty: &str, ms: &[&str]| -> Vec<(String, &'static str)> {
9374 ms.iter().map(|m| (format!("sea_orm::{ty}::{m}"), "Db")).collect()
9375 };
9376 let mut want: Vec<(String, &'static str)> = Vec::new();
9377 want.extend(db("DatabaseConnection", &["begin_with_options", "transaction_async", "transaction_with_config_async",
9378 "execute_raw", "query_one_raw", "query_all_raw", "stream_raw"]));
9379 want.extend(db("DatabaseExecutor", &["begin", "begin_with_config", "begin_with_options", "execute_raw",
9380 "query_one_raw", "query_all_raw", "transaction", "transaction_async", "transaction_with_config",
9381 "transaction_with_config_async"]));
9382 want.extend(db("DatabaseTransaction", &["begin", "begin_with_config", "begin_with_options", "execute_raw",
9383 "query_one_raw", "query_all_raw", "stream_raw", "transaction", "transaction_async",
9384 "transaction_with_config", "transaction_with_config_async"]));
9385 want.extend(db("RestrictedConnection", &["begin", "begin_with_config", "begin_with_options", "transaction",
9386 "transaction_async", "transaction_with_config", "transaction_with_config_async"]));
9387 want.extend(db("RestrictedTransaction", &["begin", "begin_with_config", "begin_with_options", "commit",
9388 "rollback", "transaction", "transaction_async", "transaction_with_config",
9389 "transaction_with_config_async"]));
9390 for m in ["load", "add_tables", "add_crud_permissions", "add_roles", "add_role_permissions",
9391 "remove_role_permissions", "add_user_override", "add_role_hierarchy", "assign_user_role"] {
9392 want.push((format!("sea_orm::rbac::RbacContext::{m}"), "Db"));
9393 want.push((format!("sea_orm::rbac::context::RbacContext::{m}"), "Db"));
9394 }
9395 want.push(("sea_orm::rbac::schema::create_tables".into(), "Db"));
9396 for m in ["apply", "sync"] {
9397 want.push((format!("sea_orm::SchemaBuilder::{m}"), "Db"));
9398 want.push((format!("sea_orm::schema::builder::SchemaBuilder::{m}"), "Db"));
9399 }
9400 for ty in ["Select", "SelectTwo", "SelectTwoRequired", "Selector", "SelectorRaw"] {
9401 want.push((format!("sea_orm::{ty}::require_one"), "Db"));
9402 }
9403 for ty in ["Select", "SelectTwo", "SelectTwoRequired", "SelectThree", "SelectFour"] {
9404 want.push((format!("sea_orm::{ty}::stream_partial_model"), "Db"));
9405 }
9406 for p in ["sea_orm::DatabaseConnection::get_schema_registry", "sea_orm::DatabaseExecutor::get_schema_registry",
9407 "sea_orm::EntityRegistry::build_schema", "sea_orm::entity::registry::EntityRegistry::build_schema"] {
9408 want.push((p.into(), "Log"));
9409 }
9410 for (p, eff) in &want {
9411 assert_eq!(classify("sea_orm", p), Some(*eff), "{p}");
9412 }
9413 // pure neighbours and lookalikes
9414 for p in [
9415 // RBAC refusals return an error without touching the connection (restricted_connection.rs:54-81)
9416 "RestrictedConnection::execute_raw", "RestrictedConnection::query_one_raw",
9417 "RestrictedConnection::query_all_raw", "RestrictedTransaction::execute_raw",
9418 // value builders / lookups
9419 "rbac::RbacContext::get_role", "SchemaBuilder::register", "Schema::new", "Schema::builder",
9420 "DatabaseConnection::get_database_backend", "DatabaseConnection::get_schema_builder",
9421 "DatabaseExecutor::is_transaction", "DatabaseExecutor::get_schema_builder",
9422 "RestrictedConnection::user_id", "RestrictedConnection::user_can", "RestrictedTransaction::user_can_run",
9423 // mock and proxy backends perform no real I/O and share every verb name
9424 "MockDatabaseConnection::begin_with_options", "MockDatabaseConnection::transaction_async",
9425 "ProxyDatabaseConnection::execute_raw", "ProxyDatabaseConnection::query_all_raw",
9426 // R1570/R1571: no consumer path reaches classify, so no rule (a rule would be dead coverage)
9427 "Vec::load_many", "Vec::load_one", "Vec::load_self", "Vec::load_self_many", "Vec::load_self_via",
9428 "Vec::load_self_via_rev", "RusqliteSharedConnection::acquire", "RusqliteSharedConnection::begin",
9429 "RusqliteSharedConnection::close", "RusqliteSharedConnection::close_by_ref",
9430 "RusqliteSharedConnection::ping", "RusqliteSharedConnection::transaction",
9431 "driver::rusqlite::RusqliteSharedConnection::ping",
9432 ] {
9433 assert_eq!(classify("sea_orm", &format!("sea_orm::{p}")), None, "{p}");
9434 }
9435 assert_eq!(classify("mycrate", "mycrate::DatabaseTransaction::execute_raw"), None);
9436 assert_eq!(classify("mycrate", "mycrate::rbac::RbacContext::load"), None);
9437 }
9438
9439 /// SOUNDNESS R1528 — the ureq 3.4.2 rows that carry a rule: every one EXECUTED against a consumer fixture
9440 /// (127.0.0.1 listener / env / file / RNG observations), both the crate-root and declaring-module spellings.
9441 #[test]
9442 fn r1528_ureq3_rows_classify() {
9443 for (p, eff) in [
9444 ("Agent::run", "Net"), ("agent::Agent::run", "Net"), ("WithAgent::run", "Net"),
9445 ("request_ext::WithAgent::run", "Net"), ("ConfigBuilder::run", "Net"),
9446 ("RequestBuilder::send_empty", "Net"), ("request::RequestBuilder::send_empty", "Net"),
9447 ("MiddlewareNext::handle", "Net"), ("middleware::MiddlewareNext::handle", "Net"),
9448 ("run", "Net"),
9449 ("DefaultResolver::resolve", "Net"), ("unversioned::resolver::DefaultResolver::resolve", "Net"),
9450 ("TcpConnector::connect", "Net"), ("unversioned::transport::TcpConnector::connect", "Net"),
9451 ("DefaultConnector::connect", "Net"), ("ConnectProxyConnector::connect", "Net"),
9452 ("SocksConnector::connect", "Net"), ("NativeTlsConnector::connect", "Net"),
9453 ("tls::native_tls::NativeTlsConnector::connect", "Net"),
9454 ("TcpTransport::transmit_output", "Net"), ("TcpTransport::await_input", "Net"),
9455 ("unversioned::transport::tcp::TcpTransport::is_open", "Net"),
9456 ("Config::builder", "Env"), ("config::Config::builder", "Env"), ("Config::default", "Env"),
9457 ("Agent::config_builder", "Env"), ("Agent::new_with_defaults", "Env"), ("Proxy::try_from_env", "Env"),
9458 ("proxy::Proxy::try_from_env", "Env"),
9459 ("Form::file", "Fs"), ("Part::file", "Fs"), ("unversioned::multipart::Part::file", "Fs"),
9460 ("File::as_body", "Fs"), ("Form::new", "Rand"), ("multipart::Form::new", "Rand"),
9461 ("Instant::now", "Clock"), ("unversioned::transport::time::Instant::now", "Clock"),
9462 ] {
9463 assert_eq!(classify("ureq", &format!("ureq::{p}")), Some(eff), "{p}");
9464 }
9465 // Rows deliberately NOT given an effect: the conditional setters, the type-argument forwarders
9466 // (R1553/R1554), the free verbs (R1555) and the two pure assemblers (`REVIEWED_PURE_ENTRIES`). Pure neighbours stay pure.
9467 for p in ["get", "post", "put", "delete", "head", "patch", "options", "trace", "connect",
9468 "ConfigBuilder::timeout_global", "ConfigBuilder::user_agent", "RequestScope::config",
9469 "Either::await_input", "Box::transmit_output", "ChainedConnector::connect",
9470 "Agent::new_with_config", "Agent::with_parts", "Agent::config", "Config::https_only",
9471 "Part::text", "Part::file_name", "Proxy::new", "Instant::duration_since"] {
9472 assert_eq!(classify("ureq", &format!("ureq::{p}")), None, "{p}");
9473 }
9474 // Another crate's same-named methods are not ureq's.
9475 assert_eq!(classify("mycrate", "mycrate::Agent::run"), None);
9476 assert_eq!(classify("reqwest", "reqwest::Config::builder"), None);
9477 }
9478
9479 /// SOUNDNESS R1528 — the async-nats 0.50.0 rows that carry a rule: each EXECUTED against a nats-server with
9480 /// JetStream (server message / byte / subscription counters around one call), under the spellings a consumer
9481 /// writes (the declaring module, the `jetstream` re-export, the `PullConsumer` alias, and the receiver a builder
9482 /// chain inherits).
9483 #[test]
9484 fn r1528_async_nats_rows_classify() {
9485 for (p, eff) in [
9486 ("jetstream::Context::create_stream", "Net"), ("jetstream::context::Context::get_stream", "Net"),
9487 ("jetstream::Context::get_or_create_stream", "Net"), ("jetstream::Context::delete_stream", "Net"),
9488 ("jetstream::Context::update_stream", "Net"), ("jetstream::Context::create_or_update_stream", "Net"),
9489 ("jetstream::Context::query_account", "Net"), ("jetstream::Context::send_publish", "Net"),
9490 ("jetstream::Context::send_request", "Net"), ("jetstream::Context::stream_by_subject", "Net"),
9491 ("jetstream::Context::create_key_value", "Net"), ("jetstream::Context::get_key_value", "Net"),
9492 ("jetstream::Context::update_key_value", "Net"), ("jetstream::Context::delete_key_value", "Net"),
9493 ("jetstream::Context::create_or_update_key_value", "Net"),
9494 ("jetstream::Context::create_object_store", "Net"), ("jetstream::Context::get_object_store", "Net"),
9495 ("jetstream::Context::delete_object_store", "Net"),
9496 ("jetstream::Context::create_consumer_on_stream", "Net"),
9497 ("jetstream::Context::create_consumer_strict_on_stream", "Net"),
9498 ("jetstream::Context::update_consumer_on_stream", "Net"),
9499 ("jetstream::Context::get_consumer_from_stream", "Net"),
9500 ("jetstream::Context::delete_consumer_from_stream", "Net"),
9501 ("jetstream::stream::Stream::info", "Net"), ("jetstream::stream::Stream::get_info", "Net"),
9502 ("jetstream::stream::Stream::info_with_subjects", "Net"),
9503 ("jetstream::stream::Stream::consumer_info", "Net"), ("jetstream::stream::Stream::create_consumer", "Net"),
9504 ("jetstream::stream::Stream::create_consumer_strict", "Net"),
9505 ("jetstream::stream::Stream::update_consumer", "Net"), ("jetstream::stream::Stream::get_consumer", "Net"),
9506 ("jetstream::stream::Stream::get_or_create_consumer", "Net"),
9507 ("jetstream::stream::Stream::delete_consumer", "Net"), ("jetstream::stream::Stream::pause_consumer", "Net"),
9508 ("jetstream::stream::Stream::resume_consumer", "Net"), ("jetstream::stream::Stream::reset_consumer", "Net"),
9509 ("jetstream::stream::Stream::delete_message", "Net"), ("jetstream::stream::Stream::purge_subject", "Net"),
9510 ("jetstream::stream::Stream::direct_get", "Net"),
9511 ("jetstream::stream::Stream::direct_get_first_for_subject", "Net"),
9512 ("jetstream::stream::Stream::direct_get_last_for_subject", "Net"),
9513 ("jetstream::stream::Stream::direct_get_next_for_subject", "Net"),
9514 ("jetstream::stream::Stream::get_raw_message", "Net"),
9515 ("jetstream::stream::Stream::get_first_raw_message_by_subject", "Net"),
9516 ("jetstream::stream::Stream::get_next_raw_message_by_subject", "Net"),
9517 ("jetstream::stream::Stream::get_last_raw_message_by_subject", "Net"),
9518 ("jetstream::stream::Stream::send", "Net"), ("jetstream::stream::Stream::fetch", "Net"),
9519 ("jetstream::stream::DirectGetBuilder::send", "Net"), ("jetstream::stream::RawMessageBuilder::send", "Net"),
9520 ("jetstream::stream::StreamInfoBuilder::fetch", "Net"),
9521 ("jetstream::kv::Store::create", "Net"), ("jetstream::kv::Store::create_with_ttl", "Net"),
9522 ("jetstream::kv::Store::put", "Net"), ("jetstream::kv::Store::get", "Net"),
9523 ("jetstream::kv::Store::entry", "Net"), ("jetstream::kv::Store::entry_for_revision", "Net"),
9524 ("jetstream::kv::Store::update", "Net"), ("jetstream::kv::Store::delete", "Net"),
9525 ("jetstream::kv::Store::delete_expect_revision", "Net"), ("jetstream::kv::Store::purge", "Net"),
9526 ("jetstream::kv::Store::purge_with_ttl", "Net"), ("jetstream::kv::Store::purge_expect_revision", "Net"),
9527 ("jetstream::kv::Store::purge_expect_revision_with_ttl", "Net"),
9528 ("jetstream::kv::Store::history", "Net"), ("jetstream::kv::Store::keys", "Net"),
9529 ("jetstream::kv::Store::watch", "Net"), ("jetstream::kv::Store::watch_many", "Net"),
9530 ("jetstream::kv::Store::watch_from_revision", "Net"), ("jetstream::kv::Store::watch_with_history", "Net"),
9531 ("jetstream::kv::Store::watch_many_with_history", "Net"), ("jetstream::kv::Store::watch_all", "Net"),
9532 ("jetstream::kv::Store::watch_all_from_revision", "Net"),
9533 ("jetstream::object_store::ObjectStore::get", "Net"), ("jetstream::object_store::ObjectStore::put", "Net"),
9534 ("jetstream::object_store::ObjectStore::info", "Net"), ("jetstream::object_store::ObjectStore::list", "Net"),
9535 ("jetstream::object_store::ObjectStore::watch", "Net"),
9536 ("jetstream::object_store::ObjectStore::watch_with_history", "Net"),
9537 ("jetstream::object_store::ObjectStore::delete", "Net"), ("jetstream::object_store::ObjectStore::seal", "Net"),
9538 ("jetstream::object_store::ObjectStore::update_metadata", "Net"),
9539 ("jetstream::object_store::ObjectStore::add_link", "Net"),
9540 ("jetstream::object_store::ObjectStore::add_bucket_link", "Net"),
9541 ("jetstream::consumer::Consumer::info", "Net"), ("jetstream::consumer::Consumer::get_info", "Net"),
9542 ("jetstream::consumer::Consumer::reset", "Net"), ("jetstream::consumer::PullConsumer::messages", "Net"),
9543 ("jetstream::consumer::PushConsumer::messages", "Net"),
9544 ("jetstream::consumer::OrderedPullConsumer::messages", "Net"),
9545 ("jetstream::consumer::OrderedPushConsumer::messages", "Net"),
9546 ("jetstream::consumer::pull::Consumer::messages", "Net"),
9547 ("jetstream::consumer::push::Consumer::messages", "Net"),
9548 ("jetstream::consumer::pull::StreamBuilder::messages", "Net"),
9549 ("jetstream::consumer::pull::FetchBuilder::messages", "Net"),
9550 ("jetstream::consumer::pull::BatchBuilder::messages", "Net"),
9551 ("jetstream::Message::double_ack", "Net"), ("jetstream::message::Message::double_ack_with", "Net"),
9552 ("jetstream::message::Acker::double_ack", "Net"), ("jetstream::message::Acker::double_ack_with", "Net"),
9553 ("jetstream::Message::ack", "Net"), ("jetstream::Message::ack_with", "Net"),
9554 ("jetstream::message::Acker::ack", "Net"), ("jetstream::message::Acker::ack_with", "Net"),
9555 ("Client::add_service", "Net"), ("service::Client::add_service", "Net"), ("Client::start", "Net"),
9556 ("Client::send_request", "Net"), ("client::Client::send_request", "Net"),
9557 ("service::ServiceBuilder::start", "Net"), ("service::Service::endpoint", "Net"),
9558 ("service::Group::endpoint", "Net"), ("service::EndpointBuilder::add", "Net"),
9559 ("service::Service::add", "Net"),
9560 ("Client::new_inbox", "Rand"), ("client::Client::new_inbox", "Rand"), ("datetime::now", "Clock"),
9561 ] {
9562 assert_eq!(classify("async_nats", &format!("async_nats::{p}")), Some(eff), "{p}");
9563 }
9564 // NOT claimed. The four R1576 rows (a `Log` rule would delete their only disclosure, see
9565 // OPEN_ENTRY_VERDICTS); calls EXECUTED to touch no broker (`get_stream_no_info` builds an empty handle);
9566 // cached/local reads and builder constructors; the wrong module for a type of the same name.
9567 for p in ["jetstream::new", "jetstream::with_domain", "jetstream::with_prefix",
9568 "jetstream::context::ContextBuilder::build", "jetstream::ContextBuilder::new",
9569 "jetstream::Context::get_stream_no_info", "jetstream::Context::wait_for_acks",
9570 "jetstream::Context::set_timeout", "jetstream::kv::Store::status",
9571 "jetstream::stream::Stream::cached_info", "jetstream::stream::Stream::info_builder",
9572 "jetstream::stream::Stream::direct_get_builder", "jetstream::stream::Stream::raw_message_builder",
9573 "jetstream::consumer::PullConsumer::stream", "jetstream::consumer::PullConsumer::fetch",
9574 "jetstream::consumer::PullConsumer::batch", "jetstream::consumer::Consumer::cached_info",
9575 "service::Service::info", "service::Service::stats", "service::Service::reset",
9576 "service::Service::group", "service::Service::endpoint_builder", "Client::timeout",
9577 "Client::max_payload", "jetstream::consumer::pull::Stream::info", "other::Stream::info",
9578 "jetstream::object_store::Object::info", "jetstream::stream::Stream::name"] {
9579 assert_eq!(classify("async_nats", &format!("async_nats::{p}")), None, "{p}");
9580 }
9581 // Another crate's same-named methods are not async-nats's.
9582 assert_eq!(classify("mycrate", "mycrate::jetstream::Context::create_stream"), None);
9583 assert_eq!(classify("redis", "redis::kv::Store::put"), None);
9584 }
9585
9586 /// SOUNDNESS R1528 — the 45 rdkafka 0.39.0 rows given rules, every one EXECUTED against a real Kafka broker
9587 /// (counting proxy bytes / loopback listener / two clock reads), in the crate-root, re-exporting-module and
9588 /// declaring-module spellings; the 24 rows with a filed verdict and their pure neighbours stay unclaimed.
9589 #[test]
9590 fn r1528_rdkafka_rows_classify() {
9591 let mut net: Vec<String> = vec![
9592 "ClientConfig::create".into(), "config::ClientConfig::create".into(), "ClientConfig::create_with_context".into(),
9593 "config::ClientConfig::create_with_context".into(), "Client::new".into(), "client::Client::new".into(),
9594 "MockCluster::new".into(), "mocking::MockCluster::new".into(),
9595 ];
9596 for v in ["create_topics", "delete_topics", "create_partitions", "delete_groups", "delete_records",
9597 "describe_configs", "alter_configs", "from_config", "from_config_and_context"] {
9598 net.push(format!("AdminClient::{v}"));
9599 net.push(format!("admin::AdminClient::{v}"));
9600 }
9601 for (ty, module) in [("BaseConsumer", "consumer::base_consumer"), ("StreamConsumer", "consumer::stream_consumer")] {
9602 for v in ["from_config", "from_config_and_context", "committed", "offsets_for_timestamp",
9603 "incremental_assign", "seek_partitions"] {
9604 net.push(format!("{ty}::{v}"));
9605 net.push(format!("consumer::{ty}::{v}"));
9606 net.push(format!("{module}::{ty}::{v}"));
9607 }
9608 }
9609 for (ty, module) in [("BaseProducer", "producer::base_producer"), ("ThreadedProducer", "producer::base_producer"),
9610 ("FutureProducer", "producer::future_producer")] {
9611 for v in ["from_config", "from_config_and_context", "init_transactions", "commit_transaction",
9612 "abort_transaction", "send_offsets_to_transaction"] {
9613 net.push(format!("{ty}::{v}"));
9614 net.push(format!("producer::{ty}::{v}"));
9615 net.push(format!("{module}::{ty}::{v}"));
9616 }
9617 }
9618 for p in &net {
9619 assert_eq!(classify("rdkafka", &format!("rdkafka::{p}")), Some("Net"), "{p}");
9620 }
9621 for p in ["Timestamp::now", "message::Timestamp::now", "util::current_time_millis"] {
9622 assert_eq!(classify("rdkafka", &format!("rdkafka::{p}")), Some("Clock"), "{p}");
9623 }
9624 // EXECUTED to move no byte on a quiet client (local state / value builders), or filed (R1581/R1582/R1583).
9625 for p in ["BaseConsumer::assignment", "BaseConsumer::subscription", "BaseConsumer::position",
9626 "BaseConsumer::incremental_unassign", "StreamConsumer::assignment", "StreamConsumer::position",
9627 "BaseProducer::begin_transaction", "ThreadedProducer::begin_transaction",
9628 "FutureProducer::begin_transaction", "BaseConsumer::close_queue",
9629 "ClientConfig::create_native_config", "TopicPartitionList::new", "TopicPartitionList::with_capacity",
9630 "TopicPartitionList::from_topic_map", "OwnedHeaders::new", "OwnedHeaders::new_with_capacity",
9631 "BorrowedHeaders::detach", "BorrowedMessage::detach", "FutureProducerContext::log",
9632 "FutureProducerContext::error", "FutureProducerContext::stats", "FutureProducerContext::stats_raw",
9633 // the wrong module / receiver for a name that IS claimed elsewhere
9634 "other::AdminClient::create_topics", "Consumer::committed", "consumer::BaseProducer::committed",
9635 "producer::BaseConsumer::init_transactions", "Foo::from_config", "Timestamp::new",
9636 "other::util::current_time_millis"] {
9637 assert_eq!(classify("rdkafka", &format!("rdkafka::{p}")), None, "{p}");
9638 }
9639 assert_eq!(classify("mycrate", "mycrate::AdminClient::create_topics"), None);
9640 assert_eq!(classify("lapin", "lapin::ClientConfig::create"), None);
9641 }
9642
9643 /// SOUNDNESS R1528 — the 36 mongodb 3.9.1 rows given rules, in the crate-root, `sync` and declaring-module
9644 /// spellings, the creators a consumer really calls AND the macro-input spellings the generator guesses; the 8 rows
9645 /// with a filed verdict (R1584, R1585) and their pure neighbours stay unclaimed.
9646 #[test]
9647 fn r1528_mongodb_rows_classify() {
9648 let mut db: Vec<String> = vec![];
9649 for m in ["", "sync::"] {
9650 for (ty, v) in [
9651 ("ClientSession", "commit_transaction"), ("ClientSession", "abort_transaction"), ("ClientSession", "and_run"),
9652 ("ClientSession", "and_run2"), ("Client", "shutdown"), ("Client", "bulk_write"), ("Database", "drop"),
9653 ("Database", "run_cursor_command"), ("Collection", "drop"), ("Collection", "drop_index"),
9654 ("Collection", "drop_indexes"), ("Collection", "list_indexes"), ("Collection", "list_index_names"),
9655 ("Collection", "create_search_index"), ("Collection", "create_search_indexes"),
9656 ("ChangeStream", "next_if_any"), ("SessionChangeStream", "next"), ("SessionChangeStream", "next_if_any"),
9657 ("GridFsBucket", "delete"), ("GridFsUploadStream", "abort"),
9658 ] {
9659 db.push(format!("{m}{ty}::{v}"));
9660 }
9661 }
9662 for p in [
9663 "change_stream::ChangeStream::next_if_any", "change_stream::session::SessionChangeStream::next",
9664 "change_stream::session::SessionChangeStream::next_if_any", "sync::change_stream::ChangeStream::next_if_any",
9665 "sync::change_stream::SessionChangeStream::next", "sync::change_stream::SessionChangeStream::next_if_any",
9666 "action::shutdown::Client::shutdown", "gridfs::GridFsBucket::delete", "gridfs::GridFsUploadStream::abort",
9667 // the macro-input spellings (generator rows)
9668 "client::session::action::StartTransaction::and_run", "client::session::action::StartTransaction::and_run2",
9669 "client::session::action::AbortTransaction::execute", "client::session::action::CommitTransaction::execute",
9670 "client::action::shutdown::Shutdown::execute", "action::bulk_write::BulkWrite::execute",
9671 "action::count::CountDocuments::execute", "db::action::create_collection::CreateCollection::execute",
9672 "action::search_index::CreateSearchIndex::execute", "coll::action::drop::DropCollection::execute",
9673 "action::drop::DropDatabase::execute", "action::drop_index::DropIndex::execute", "action::find::Find::execute",
9674 "action::find::Find::batch", "action::find_and_modify::FindOneAndDelete::execute",
9675 "action::find_and_modify::FindOneAndReplace::execute", "action::find_and_modify::FindOneAndUpdate::execute",
9676 "action::list_collections::ListCollections::execute", "action::list_collections::ListCollections::batch",
9677 "action::list_databases::ListDatabases::execute", "action::list_indexes::ListIndexes::execute",
9678 "action::run_command::RunCommand::execute", "action::run_command::RunCursorCommand::execute",
9679 "action::run_command::RunCursorCommand::batch", "action::watch::Watch::execute", "Find::execute",
9680 "action::gridfs::delete::Delete::execute",
9681 ] {
9682 db.push(p.to_string());
9683 }
9684 for p in &db {
9685 assert_eq!(classify("mongodb", &format!("mongodb::{p}")), Some("Db"), "{p}");
9686 }
9687 for p in ["Callback::human", "Callback::machine", "options::oidc::Callback::human", "client::auth::oidc::Callback::machine"] {
9688 assert_eq!(classify("mongodb", &format!("mongodb::{p}")), Some("Clock"), "{p}");
9689 }
9690 // EXECUTED to move no byte (`start_transaction`), read and filed (R1584, R1585), or pure neighbours.
9691 for p in [
9692 "ClientSession::start_transaction", "ClientSession::end_session", "Collection::name", "Database::name",
9693 "Collection::namespace", "options::ClientOptions::parse", "ClientOptions::parse", "options::ConnectionString::parse",
9694 "ConnectionString::parse", "client::auth::AuthMechanism::validate_credential", "AuthMechanism::validate_credential",
9695 "client::options::parse::ParseConnectionString::execute", "ClientEncryption::new",
9696 "client::csfle::client_encryption::ClientEncryptionBuilder::build", "client::csfle::client_builder::EncryptedClientBuilder::build",
9697 "client::csfle::client_encryption::create_data_key::CreateDataKey::execute", "client::csfle::client_encryption::encrypt::Encrypt::execute",
9698 // wrong receiver / module for a name that IS claimed elsewhere
9699 "Foo::commit_transaction", "Collection::commit_transaction", "other::Client::shutdown", "Delete::execute",
9700 "action::delete::Delete::execute", "Callback::new", "other::Callback::human", "ChangeStream::next", "Cursor::next_if_any",
9701 ] {
9702 assert_eq!(classify("mongodb", &format!("mongodb::{p}")), None, "{p}");
9703 }
9704 assert_eq!(classify("mycrate", "mycrate::ClientSession::commit_transaction"), None);
9705 assert_eq!(classify("mysql_async", "mysql_async::ClientSession::commit_transaction"), None);
9706 }
9707
9708 /// SOUNDNESS R1528 — the mysql_async 0.37.1 rows given rules (31 of 38; 5 read pure, 2 filed R1586), in the
9709 /// crate-root and declaring-module spellings, plus the typed / bound spellings of the `Query` family that a scan can
9710 /// name. The `map` name stays unclaimed on purpose: the scan attributes every `.map` off a typed root to that root.
9711 #[test]
9712 fn r1528_mysql_async_rows_classify() {
9713 let mut db: Vec<String> = vec![];
9714 for (ty, ms, vs) in [
9715 ("Conn", vec!["", "conn::", "conn::binlog_stream::", "queryable::"], vec!["change_user", "reset", "disconnect", "get_binlog_stream", "close", "start_transaction"]),
9716 ("BinlogStream", vec!["", "conn::binlog_stream::"], vec!["close"]),
9717 ("Connection", vec!["", "queryable::"], vec!["close"]),
9718 ("Transaction", vec!["", "queryable::", "queryable::transaction::"], vec!["close", "commit", "rollback"]),
9719 ("Pool", vec!["", "conn::pool::", "connection_like::"], vec!["start_transaction", "to_connection"]),
9720 ("QueryResult", vec!["", "queryable::query_result::", "queryable::query_result::result_set_stream::"],
9721 vec!["next", "collect", "try_collect", "collect_and_drop", "try_collect_and_drop", "for_each", "for_each_and_drop",
9722 "map_and_drop", "reduce", "reduce_and_drop", "drop_result", "stream", "stream_and_drop"]),
9723 ("QueryWithParams", vec!["", "query::"], vec!["run", "batch", "first", "fetch", "ignore", "reduce", "stream"]),
9724 ("Query", vec!["prelude::"], vec!["run", "first", "fetch", "ignore", "reduce", "stream"]),
9725 ("BatchQuery", vec!["prelude::"], vec!["batch"]),
9726 ("Q", vec!["", "query::"], vec!["run"]),
9727 ] {
9728 for m in &ms {
9729 for v in &vs {
9730 db.push(format!("{m}{ty}::{v}"));
9731 }
9732 }
9733 }
9734 for p in &db {
9735 assert_eq!(classify("mysql_async", &format!("mysql_async::{p}")), Some("Db"), "{p}");
9736 }
9737 for p in ["test_misc::get_opts", "test_misc::test_compression", "test_misc::test_ssl"] {
9738 assert_eq!(classify("mysql_async", &format!("mysql_async::{p}")), Some("Env"), "{p}");
9739 }
9740 // existing rules untouched
9741 assert_eq!(classify("mysql_async", "mysql_async::Conn::new"), Some("Db"));
9742 assert_eq!(classify("mysql_async", "mysql_async::Pool::get_conn"), Some("Db"));
9743 // filed (R1586), read pure (to_statement), or pure neighbours; wrong receiver for a claimed name
9744 for p in [
9745 "QueryResult::map", "queryable::query_result::QueryResult::map", "QueryWithParams::map", "prelude::Query::map",
9746 "opts::PathOrBuf::read", "PathOrBuf::read", "Arc::to_statement", "String::to_statement", "Vec::to_statement",
9747 "prelude::StatementLike::to_statement", "Conn::map", "Conn::opts", "Conn::tcp_port", "Pool::opts", "Pool::iter",
9748 "Transaction::last_insert_id", "TxOpts::new", "TxOpts::with_readonly", "Opts::from_url", "Params::from",
9749 "QueryResult::columns_ref", "QueryResult::columns", "QueryResult::affected_rows", "QueryResult::info",
9750 "Foo::commit", "Pool::commit", "Conn::commit", "test_misc::other", "other::test_misc::get_opts",
9751 "Query::map", "BatchQuery::run",
9752 ] {
9753 assert_eq!(classify("mysql_async", &format!("mysql_async::{p}")), None, "{p}");
9754 }
9755 // R1528 (b10): this asserted `None` as a name-collision control; EXECUTED against MySQL 8, `Conn::reset` sends
9756 // COM_RESET_CONNECTION (5 bytes up / 11 down), so the mysql one is `Db`. The mongodb one below is not claimed.
9757 assert_eq!(classify("mysql", "mysql::Conn::reset"), Some("Db"));
9758 assert_eq!(classify("mongodb", "mongodb::Conn::reset"), None);
9759 }
9760
9761 /// SOUNDNESS R1528 — the aws-config 1.12.0 rows given rules (19 of 23 plus three siblings the generator never
9762 /// listed), in the declaring-module spelling and the root-typed spelling the scan produces for a chain. The
9763 /// negatives are the point: the scan attributes every hop off a typed root to the root, so `.region(..)` on a
9764 /// credentials builder arrives as `DefaultCredentialsChain::region`, and a bare `region` / `build` rule would
9765 /// fabricate on setters and on pure builders.
9766 #[test]
9767 fn r1528_aws_config_rows_classify() {
9768 for p in [
9769 "default_provider::endpoint_url::endpoint_url_provider",
9770 "default_provider::endpoint_url::endpoint_url_provider_with_origin",
9771 "default_provider::ignore_configured_endpoint_urls::ignore_configured_endpoint_urls_provider",
9772 "default_provider::checksums::request_checksum_calculation_provider",
9773 "default_provider::checksums::response_checksum_validation_provider",
9774 "default_provider::use_dual_stack::use_dual_stack_provider",
9775 "default_provider::use_fips::use_fips_provider",
9776 "default_provider::app_name::Builder::app_name",
9777 "default_provider::retry_config::Builder::retry_config",
9778 "profile::region::ProfileFileRegionProvider::region",
9779 ] {
9780 assert_eq!(classify("aws_config", &format!("aws_config::{p}")), Some("Fs"), "{p}");
9781 }
9782 for p in [
9783 "default_provider::region::DefaultRegionChain::region",
9784 "imds::region::ImdsRegionProvider::region",
9785 "provider_config::ProviderConfig::load_default_region",
9786 "provider_config::ProviderConfig::with_default_region",
9787 "default_provider::credentials::DefaultCredentialsChain::build",
9788 "default_provider::credentials::Builder::build",
9789 "default_provider::credentials::default_provider",
9790 "default_provider::token::DefaultTokenChain::build",
9791 "default_provider::token::Builder::build",
9792 "default_provider::token::default_provider",
9793 "sts::AssumeRoleProvider::build",
9794 "sts::AssumeRoleProvider::build_from_provider",
9795 "sts::assume_role::AssumeRoleProviderBuilder::build",
9796 "sts::assume_role::AssumeRoleProviderBuilder::build_from_provider",
9797 "sso::token::SsoTokenProvider::build",
9798 "sso::token::Builder::build",
9799 "ecs::EcsCredentialsProvider::credentials",
9800 ] {
9801 assert_eq!(classify("aws_config", &format!("aws_config::{p}")), Some("Net"), "{p}");
9802 }
9803 // filed (R1588), setters the scan attributes to a root, pure builders, and the existing `.load()` rule
9804 for p in [
9805 "meta::region::Box::region", "meta::region::RegionProviderChain::region",
9806 "profile::credentials::Builder::build", "profile::token::Builder::build",
9807 "profile::ProfileFileCredentialsProvider::build", "profile::ProfileFileTokenProvider::build",
9808 "default_provider::credentials::DefaultCredentialsChain::region",
9809 "default_provider::token::DefaultTokenChain::region",
9810 "sso::token::SsoTokenProvider::region", "sso::token::SsoTokenProvider::session_name",
9811 "default_provider::region::DefaultRegionChain::build",
9812 "default_provider::region::DefaultRegionChain::profile_name",
9813 "default_provider::region::default_provider", "default_provider::timeout_config::Builder::timeout_config",
9814 "provider_config::ProviderConfig::empty", "provider_config::ProviderConfig::with_region",
9815 "sts::AssumeRoleProvider::configure", "sts::AssumeRoleProvider::builder",
9816 "imds::region::ImdsRegionProvider::build", "ecs::EcsCredentialsProvider::build",
9817 "default_provider::use_fips::other", "other::default_provider::credentials::DefaultCredentialsChain::build",
9818 ] {
9819 assert_eq!(classify("aws_config", &format!("aws_config::{p}")), None, "{p}");
9820 }
9821 assert_eq!(classify("aws_config", "aws_config::loader::ConfigLoader::load"), Some("Net"));
9822 assert_eq!(classify("mycrate", "mycrate::default_provider::use_fips::use_fips_provider"), None);
9823 }
9824
9825 /// SOUNDNESS R1528 — the sqlx-core 0.9.0 rows given rules (10 of 22; 2 read pure, 8 conditional and 2 silent filed),
9826 /// under the `sqlx_core` spelling and the `sqlx` facade spelling a facade consumer actually produces.
9827 #[test]
9828 fn r1528_sqlx_core_rows_classify() {
9829 for (k, p, e) in [
9830 ("sqlx_core", "pool::Pool::begin_with", "Db"), ("sqlx_core", "Pool::begin_with", "Db"),
9831 ("sqlx", "PgPool::begin_with", "Db"), ("sqlx", "Pool::begin_with", "Db"), ("sqlx", "MySqlPool::begin_with", "Db"),
9832 ("sqlx_core", "pool::PoolConnection::close", "Net"), ("sqlx", "pool::PoolConnection::close", "Net"),
9833 ("sqlx_core", "pool::connection::PoolConnection::close", "Net"),
9834 ("sqlx_core", "pool::PoolConnection::return_to_pool", "Db"), ("sqlx", "pool::PoolConnection::return_to_pool", "Db"),
9835 ("sqlx_core", "net::connect_tcp", "Net"), ("sqlx_core", "net::socket::connect_tcp", "Net"),
9836 ("sqlx_core", "migrate::Migrator::new", "Fs"), ("sqlx_core", "migrate::migrator::Migrator::new", "Fs"),
9837 ("sqlx", "migrate::Migrator::new", "Fs"),
9838 ("sqlx_core", "fs::ReadDir::next", "Fs"),
9839 ("sqlx_core", "migrate::source::resolve_blocking", "Fs"),
9840 ("sqlx_core", "migrate::source::resolve_blocking_with_config", "Fs"),
9841 ("sqlx_core", "migrate::resolve_blocking", "Fs"),
9842 ("sqlx_core", "logger::QueryLogger::new", "Clock"), ("sqlx_core", "logger::QueryLogger::finish", "Log"),
9843 ] {
9844 assert_eq!(classify(k, &format!("{k}::{p}")), Some(e), "{k}::{p}");
9845 }
9846 // read pure (detach, leak), filed (connect_lazy, Box poll_*, resolve) and pure neighbours stay unclaimed
9847 for (k, p) in [
9848 ("sqlx_core", "pool::PoolConnection::detach"), ("sqlx_core", "pool::PoolConnection::leak"),
9849 ("sqlx", "pool::PoolConnection::detach"), ("sqlx", "pool::PoolConnection::leak"),
9850 ("sqlx_core", "pool::Pool::connect_lazy"), ("sqlx_core", "pool::Pool::connect_lazy_with"),
9851 ("sqlx_core", "pool::PoolOptions::connect_lazy"), ("sqlx_core", "pool::PoolOptions::connect_lazy_with"),
9852 // R1623: `PgPool::connect_lazy(url)` / `PgPoolOptions::connect_lazy(url)` are NOT pure: they parse the URL (12 PG* getenv,
9853 // EXECUTED), charged Env + Fs; the 0 CONNECTIONS of R1589 stands. `MySqlPool::connect_lazy` parses nothing: pure.
9854 ("sqlx", "MySqlPool::connect_lazy"), ("sqlx", "sqlite::SqlitePool::connect_lazy"),
9855 ("sqlx_core", "net::socket::Box::poll_flush"), ("sqlx_core", "net::Socket::poll_read_ready"),
9856 ("sqlx_core", "migrate::source::PathBuf::resolve"), ("sqlx_core", "migrate::source::ResolveWith::resolve"),
9857 ("sqlx_core", "migrate::ResolveConfig::new"), ("sqlx_core", "logger::QueryLogger::sql"),
9858 ("sqlx_core", "logger::QueryLogger::increment_rows_returned"),
9859 ("sqlx_core", "pool::PoolOptions::max_connections"), ("sqlx_core", "pool::PoolConnection::close_on_drop"),
9860 ("sqlx", "postgres::PgPoolOptions::new"), ("sqlx", "PgPool::size"), ("sqlx", "other::PgPool::begin_with"),
9861 ("sqlx", "Connection::close"), ("sqlx", "Transaction::new"),
9862 ] {
9863 assert_eq!(classify(k, &format!("{k}::{p}")), None, "{k}::{p}");
9864 }
9865 // `net::connect_tcp` is the `sqlx_core` re-export; a facade spelling of the same name is not claimed
9866 assert_eq!(classify("sqlx", "sqlx::net::connect_tcp"), None);
9867 assert_eq!(classify("mycrate", "mycrate::pool::PoolConnection::close"), None);
9868 }
9869
9870 /// SOUNDNESS R1528 — the redis 1.7.1 rows given rules (13 of 21; 2 read pure, 6 filed), in the crate-root, the
9871 /// re-export and the declaring-module spellings. `Sentinel::build` was a deliberate `None` (R335: "parses URLs");
9872 /// it draws a random replica index but a `Rand` rule there deleted the disclosure of the `Db` that follows (R1590:silent), so it stays unclaimed.
9873 #[test]
9874 fn r1528_redis_rows_classify() {
9875 for (p, e) in [
9876 ("Client::is_open", "Db"), ("client::Client::is_open", "Db"), ("Client::check_connection", "Db"),
9877 ("Client::get_async_pubsub", "Db"), ("client::Client::get_async_monitor", "Db"),
9878 ("Cmd::iter_async", "Db"), ("cmd::Cmd::iter_async", "Db"),
9879 ("aio::MultiplexedConnection::send_packed_command", "Db"),
9880 ("aio::MultiplexedConnection::send_packed_commands", "Db"),
9881 ("aio::multiplexed_connection::MultiplexedConnection::send_packed_command", "Db"),
9882 ("aio::MultiplexedConnection::connect_with_config", "Db"),
9883 ("cluster_handling::async_connection::MultiplexedConnection::connect_with_config", "Db"),
9884 ("cluster::ClusterConnection::route_command", "Db"), ("cluster_async::ClusterConnection::route_command", "Db"),
9885 ("cluster_handling::sync_connection::ClusterConnection::route_command", "Db"),
9886 ("cluster_handling::async_connection::ClusterConnection::route_command", "Db"),
9887 ("cluster::ClusterPipeline::exec", "Db"), ("cluster_handling::sync_connection::pipeline::ClusterPipeline::exec", "Db"),
9888 ("cluster::ClusterClient::get_pending_async_connection_with_config", "Db"),
9889 ("cluster_handling::client::ClusterClient::get_pending_async_connection_with_config", "Db"),
9890 ("cluster_read_routing::Replicas::choose_random", "Rand"),
9891 ("cluster_handling::read_routing::interface::Replicas::choose_random", "Rand"),
9892 ("cluster_read_routing::RandomReplicaStrategy::route_read", "Rand"),
9893 ("cluster_handling::read_routing::random_replica::RandomReplicaStrategy::route_read", "Rand"),
9894 ] {
9895 assert_eq!(classify("redis", &format!("redis::{p}")), Some(e), "{p}");
9896 }
9897 // read pure (build_with_tls, cluster builder build), filed (new / new_with_config / entra start), neighbours;
9898 // the receiver-name guards: `is_open` on a Connection, `route_command` on a non-cluster type
9899 for p in [
9900 "Client::build_with_tls", "client::Client::build_with_tls", "cluster::ClusterClient::build",
9901 "cluster_handling::client::ClusterClientBuilder::build", "cluster::ClusterClientBuilder::retries",
9902 "aio::MultiplexedConnection::new", "aio::MultiplexedConnection::new_with_config",
9903 "aio::multiplexed_connection::MultiplexedConnection::new",
9904 "entra_id::EntraIdCredentialsProvider::start", "Client::open", "Client::get_connection_info",
9905 "Connection::is_open", "aio::MultiplexedConnection::is_open", "Other::route_command",
9906 "cluster::ClusterClient::builder", "cluster_read_routing::Replicas::new", "cluster_read_routing::Replicas::iter",
9907 "sentinel::Sentinel::new", "sentinel::SentinelClientBuilder::set_client_to_redis_db",
9908 "sentinel::Sentinel::build", "sentinel::SentinelClient::build", "sentinel::SentinelClientBuilder::build",
9909 "other::Client::is_open", "Cmd::arg", "Cmd::new",
9910 ] {
9911 assert_eq!(classify("redis", &format!("redis::{p}")), None, "{p}");
9912 }
9913 assert_eq!(classify("mycrate", "mycrate::Client::is_open"), None);
9914 }
9915
9916 /// SOUNDNESS R1528 — the 18 reachable rusqlite rows, every one EXECUTED against a consumer fixture, with
9917 /// same-type pure neighbours left unclaimed. Both the crate-root and the declaring-module spelling.
9918 #[test]
9919 fn r1528_rusqlite_reachable_rows_classify() {
9920 for p in ["Connection::close", "Connection::cache_flush", "Connection::serialize", "Connection::deserialize",
9921 "Connection::deserialize_bytes", "Connection::deserialize_read_exact",
9922 "Connection::load_extension", "Connection::load_extension_enable",
9923 "Connection::load_extension_disable", "Connection::query_row_and_then",
9924 "Connection::prepare_with_flags", "Statement::exists", "LoadExtensionGuard::new",
9925 "Wal::checkpoint", "Wal::checkpoint_v2", "serialize::Connection::serialize",
9926 "statement::Statement::exists", "hooks::Wal::checkpoint_v2",
9927 "load_extension_guard::LoadExtensionGuard::new",
9928 "register_auto_extension", "cancel_auto_extension", "reset_auto_extension",
9929 "auto_extension::register_auto_extension", "auto_extension::cancel_auto_extension",
9930 "auto_extension::reset_auto_extension"] {
9931 assert_eq!(classify("rusqlite", &format!("rusqlite::{p}")), Some("Db"), "{p}");
9932 }
9933 // The two SQL-text verbs are table members, so the masking guard reads them (SQL at argument 0).
9934 for v in ["query_row_and_then", "prepare_with_flags"] {
9935 assert!(masks_locator("Db", "rusqlite", &format!("rusqlite::Connection::{v}"), v, true), "{v}");
9936 }
9937 // Same-type neighbours that touch no database stay unclaimed.
9938 for p in ["Connection::is_autocommit", "Connection::changes", "Connection::last_insert_rowid",
9939 "Connection::is_busy", "Connection::is_readonly", "Connection::path", "Statement::column_count",
9940 "Wal::name", "LoadExtensionGuard::drop", "Connection::handle"] {
9941 assert_eq!(classify("rusqlite", &format!("rusqlite::{p}")), None, "{p}");
9942 }
9943 // Another crate's same-named method is not rusqlite's.
9944 assert_eq!(classify("mycrate", "mycrate::Connection::close"), None);
9945 assert_eq!(classify("tokio_postgres", "tokio_postgres::Connection::close"), None);
9946 }
9947
9948 /// SOUNDNESS R1528 (isahc 2.0.1) — the HTTP client method spellings and `TrustStore::native`, in the root-typed
9949 /// spelling the scan produces and the declaring-module one the manifest row names. The negatives are the cookie jar and
9950 /// `Response::text` (filed R1592, conditional) and the same names on another crate.
9951 #[test]
9952 fn r1528_isahc_rows_classify() {
9953 for m in ["get_async", "head_async", "post_async", "put_async", "delete_async", "send_async"] {
9954 assert_eq!(classify("isahc", &format!("isahc::HttpClient::{m}")), Some("Net"), "{m}");
9955 assert_eq!(classify("isahc", &format!("isahc::client::HttpClient::{m}")), Some("Net"), "{m}");
9956 }
9957 assert_eq!(classify("isahc", "isahc::Request::send_async"), Some("Net"));
9958 assert_eq!(classify("isahc", "isahc::request::Request::send_async"), Some("Net"));
9959 assert_eq!(classify("isahc", "isahc::tls::TrustStore::native"), Some("Env"));
9960 assert_eq!(classify("isahc", "isahc::tls::trust::TrustStore::native"), Some("Env"));
9961 for p in ["cookies::CookieJar::get_by_name", "cookies::CookieJar::get_for_uri", "cookies::CookieJar::set",
9962 "cookies::jar::CookieJar::set", "Response::text", "response::Response::text", "HttpClient::builder",
9963 "TrustStore::webpki_roots", "TrustStore::from_file", "tls::TrustStore::from_file"] {
9964 assert_eq!(classify("isahc", &format!("isahc::{p}")), None, "{p}");
9965 }
9966 assert_eq!(classify("mycrate", "mycrate::HttpClient::get_async"), None);
9967 }
9968
9969 /// SOUNDNESS R1528 (lettre 0.11.23) — both the `MessageBuilder` (typed receiver) and the root `Message` (chain)
9970 /// spellings, since the scan attributes `Message::builder().from(..).body(..)` to `Message::body`.
9971 #[test]
9972 fn r1528_lettre_rows_classify() {
9973 for t in ["MessageBuilder", "Message"] {
9974 for m in ["body", "singlepart", "multipart", "date_now"] {
9975 assert_eq!(classify("lettre", &format!("lettre::message::{t}::{m}")), Some("Clock"), "{t}::{m}");
9976 }
9977 assert_eq!(classify("lettre", &format!("lettre::message::{t}::message_id")), Some("Rand"), "{t}");
9978 }
9979 for p in ["message::Message::sign", "message::header::Date::now", "message::header::date::Date::now",
9980 "message::dkim::dkim_sign"] {
9981 assert_eq!(classify("lettre", &format!("lettre::{p}")), Some("Clock"), "{p}");
9982 }
9983 for p in ["message::MultiPartBuilder::kind", "message::MultiPart::kind", "message::mimebody::MultiPartBuilder::kind",
9984 "message::MultiPart::mixed", "message::MultiPart::alternative", "message::MultiPart::related",
9985 "message::MultiPart::signed", "message::MultiPart::encrypted", "message::MultiPart::alternative_plain_html"] {
9986 assert_eq!(classify("lettre", &format!("lettre::{p}")), Some("Rand"), "{p}");
9987 }
9988 for p in ["AsyncFileTransport::read", "transport::file::AsyncFileTransport::read", "Executor::fs_read",
9989 "Executor::fs_write", "Tokio1Executor::fs_read", "executor::Tokio1Executor::fs_write",
9990 "AsyncStd1Executor::fs_read"] {
9991 assert_eq!(classify("lettre", &format!("lettre::{p}")), Some("Fs"), "{p}");
9992 }
9993 // Filed R1592 / read pure: no rule. And the pure siblings of the Clock / Rand rows stay unclaimed.
9994 for p in ["transport::smtp::SmtpTransportBuilder::build", "transport::smtp::AsyncSmtpTransportBuilder::build",
9995 "transport::smtp::transport::SmtpTransport::unencrypted_localhost",
9996 "transport::smtp::async_transport::AsyncSmtpTransport::unencrypted_localhost",
9997 "transport::smtp::commands::Auth::new_from_response", "message::Message::builder",
9998 "message::Message::to", "message::Message::subject", "message::MessageBuilder::subject",
9999 "message::MessageBuilder::date", "message::Message::date", "message::MultiPart::builder",
10000 "message::MultiPart::singlepart", "message::MultiPartBuilder::boundary", "message::Message::formatted",
10001 "message::header::Date::new", "message::SinglePart::plain"] {
10002 assert_eq!(classify("lettre", &format!("lettre::{p}")), None, "{p}");
10003 }
10004 assert_eq!(classify("mycrate", "mycrate::message::Message::body"), None);
10005 }
10006
10007 /// SOUNDNESS R1528 (lapin 4.12.0) — the connection openers, `update_secret`, the two generated channel verbs and the
10008 /// acker. `connector`, `run`, `set_delegate` (filed R1593, conditional) and `wait_for_confirms` (read pure) stay
10009 /// unclaimed, as do same-named methods on other types (`Channel::reject`, `Connection::ack`).
10010 #[test]
10011 fn r1528_lapin_rows_classify() {
10012 for m in ["connect_uri", "connect_uri_with_runtime", "connect_uri_with_config", "connect_with_config",
10013 "connect_with_runtime", "update_secret"] {
10014 assert_eq!(classify("lapin", &format!("lapin::Connection::{m}")), Some("Net"), "{m}");
10015 assert_eq!(classify("lapin", &format!("lapin::connection::Connection::{m}")), Some("Net"), "{m}");
10016 }
10017 for p in ["AMQPUri::connect_with_config", "uri::AMQPUri::connect_with_config",
10018 "connection::AMQPUri::connect_with_config", "Connect::connect_with_config"] {
10019 assert_eq!(classify("lapin", &format!("lapin::{p}")), Some("Net"), "{p}");
10020 }
10021 for m in ["access_request", "channel_flow"] {
10022 assert_eq!(classify("lapin", &format!("lapin::Channel::{m}")), Some("Net"), "{m}");
10023 assert_eq!(classify("lapin", &format!("lapin::generated::channel::Channel::{m}")), Some("Net"), "{m}");
10024 }
10025 for m in ["ack", "nack", "reject"] {
10026 assert_eq!(classify("lapin", &format!("lapin::Acker::{m}")), Some("Net"), "{m}");
10027 assert_eq!(classify("lapin", &format!("lapin::acker::Acker::{m}")), Some("Net"), "{m}");
10028 }
10029 for p in ["Connection::connector", "connection::Connection::connector", "Connection::run",
10030 "connection::Connection::run", "Consumer::set_delegate", "consumer::Consumer::set_delegate",
10031 "Channel::wait_for_confirms", "channel::Channel::wait_for_confirms", "Channel::reject",
10032 "Connection::ack", "Connection::status", "Channel::status", "Acker::poisoned", "Acker::usable",
10033 "ConnectionProperties::default", "runtime::default_runtime"] {
10034 assert_eq!(classify("lapin", &format!("lapin::{p}")), None, "{p}");
10035 }
10036 // The existing `::connect` rule is untouched.
10037 assert_eq!(classify("lapin", "lapin::Connection::connect"), Some("Net"));
10038 assert_eq!(classify("mycrate", "mycrate::Acker::ack"), None);
10039 }
10040
10041 /// SOUNDNESS R1528 (tonic 0.14.6) — `Server::serve{,_with_shutdown}` IS the quickstart (`Server::builder().add_service(..)
10042 /// .serve(addr)` reaches classify as `Server::serve`, the scan attributing every chain hop to the root type) and
10043 /// `TcpIncoming::bind`. The filed R1594 rows (`serve_with_incoming*`, `tls_config*`, `Streaming`, `Code`, `Status`) stay
10044 /// unclaimed.
10045 #[test]
10046 fn r1528_tonic_rows_classify() {
10047 for p in ["Server::serve", "Server::serve_with_shutdown", "transport::Server::serve", "transport::Server::serve_with_shutdown",
10048 "transport::server::Server::serve", "transport::server::Server::serve_with_shutdown",
10049 "TcpIncoming::bind", "transport::server::TcpIncoming::bind", "transport::server::incoming::TcpIncoming::bind"] {
10050 assert_eq!(classify("tonic", &format!("tonic::{p}")), Some("Net"), "{p}");
10051 }
10052 for p in ["transport::Server::serve_with_incoming", "transport::Server::serve_with_incoming_shutdown",
10053 "transport::server::Server::serve_with_incoming", "transport::server::Router::serve_with_incoming",
10054 "transport::server::Router::serve_with_incoming_shutdown",
10055 "transport::Endpoint::tls_config", "transport::Endpoint::tls_config_with_verifier",
10056 "transport::ClientTlsConfig::with_native_roots", "transport::ClientTlsConfig::use_key_log",
10057 "Code::from_bytes", "Status::from_header_map", "Streaming::message", "Streaming::trailers",
10058 "transport::Server::builder", "transport::Server::add_service", "transport::Server::tcp_nodelay",
10059 "transport::server::TcpIncoming::with_nodelay"] {
10060 assert_eq!(classify("tonic", &format!("tonic::{p}")), None, "{p}");
10061 }
10062 // Rules that predate this row stay: the Router spelling, the connect family and the old constructor.
10063 assert_eq!(classify("tonic", "tonic::transport::server::Router::serve"), Some("Net"));
10064 assert_eq!(classify("tonic", "tonic::transport::Endpoint::connect"), Some("Net"));
10065 assert_eq!(classify("mycrate", "mycrate::Server::serve"), None);
10066 }
10067}