1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//! pathlint library — verifies that commands on PATH resolve from the expected installer.
//!
//! # Public API surface (0.0.15+)
//!
//! The supported library surface is **ten modules**, each
//! described below by a few representative symbols. The
//! authoritative contract is `tests/public_api.rs`, which
//! imports every symbol pathlint promises here and fails the
//! build if any is moved or removed.
//!
//! - [`config`]: TOML schema. Headlines: `Config`, `Expectation`,
//! `SourceDef`, `Relation`, `Severity`, `Kind`.
//! - [`lint`]: core PATH evaluation. Headlines: `evaluate`,
//! `exit_code`, `Outcome`, `Status`, `Diagnosis`,
//! `CheckOutcomeView`. Resolver closures take
//! `&str -> Option<std::path::PathBuf>` (0.0.16+).
//! - [`trace`]: provenance lookup. Headlines: `locate`,
//! `TraceOutcome`, `Found`, `Provenance`, `UninstallHint`.
//! - [`sort`]: PATH repair proposals. Headlines: `sort_path`,
//! `SortPlan`, `EntryMove`, `SortNote`.
//! - [`doctor`]: PATH hygiene. Headlines: `analyze`,
//! `analyze_real`, `fs_list_dir_real` (the production wrapper
//! for the 0.0.19 `fs_list_dir` closure parameter),
//! `Diagnostic`, `Filter`, plus the `Kind` / `Severity` enums.
//! - [`catalog`]: built-in source catalog. Headlines: `builtin`,
//! `builtin_relations`, `merge_with_user`,
//! `merge_with_user_relations`, `check_acyclic`,
//! `version_check`, `embedded_version`.
//! - [`source_match`]: path → source matching. Headlines: `find`,
//! `names_only`, `validate_sources`, `Match`, `SourceWarning`.
//! - [`os_detect`]: runtime OS dispatch. Headlines: `Os`,
//! `os_filter_applies`.
//! - [`expand`]: env-var expansion + slash normalisation.
//! Headlines: `expand_env`, `normalize`, `expand_and_normalize`.
//! - [`path_entry`]: PATH entry duality (raw + expanded). Headlines:
//! `PathEntry`, `PathEntry::from_raw`. 0.0.23+.
//!
//! Anything not exported through one of those modules — including
//! the CLI plumbing, presentation layer, registry reader, and
//! orchestration glue — is **internal** and may change between
//! patch releases without notice.
//!
//! Across 0.0.x the library surface is treated as additive-only
//! best-effort; intentional breaks land at `0.0.x → 0.0.(x+1)`
//! boundaries (Cargo's MAJOR-equivalent for `0.0.y`) and are
//! flagged in release notes.
//!
//! # Quick example
//!
//! Evaluate one expectation against an in-process PATH without
//! reading a `pathlint.toml` from disk:
//!
//! ```
//! use pathlint::config::{Config, Expectation, Severity};
//! use pathlint::lint;
//! use pathlint::os_detect::Os;
//! use std::path::PathBuf;
//!
//! // Caller-supplied resolver. Production wiring would use a
//! // `which`-style PATH walk; this stub pretends `rg` lives in
//! // `~/.cargo/bin`. The closure boundary intentionally takes
//! // and returns standard-library types only — pathlint never
//! // exposes its own internal resolver type to embedders.
//! let resolver = |cmd: &str| -> Option<PathBuf> {
//! match cmd {
//! "rg" => Some(PathBuf::from("/home/me/.cargo/bin/rg")),
//! _ => None,
//! }
//! };
//!
//! let cfg = Config::default();
//! let expectations = vec![Expectation {
//! command: "rg".into(),
//! prefer: vec!["cargo".into()],
//! avoid: vec![],
//! os: None,
//! optional: false,
//! kind: None,
//! severity: Severity::Error,
//! }];
//!
//! let sources = pathlint::catalog::merge_with_user(&cfg.source);
//! let deps = lint::EvaluateDeps {
//! common: pathlint::CommonDeps { env_lookup: Box::new(|_v: &str| -> Option<String> { None }) },
//! resolver: Box::new(resolver),
//! shape_check: Box::new(lint::check_shape_filesystem), // R2 shape check; unused here
//! };
//! let outcomes = lint::evaluate(&expectations, &sources, Os::current(), deps);
//!
//! // outcomes[0].status is Status::Ok because the resolver picked
//! // a path under cargo's built-in source.
//! assert_eq!(outcomes.len(), 1);
//! ```
//!
//! For the full pipeline (read `pathlint.toml`, walk `$PATH`,
//! print to stdout, set the exit code) see the binary at
//! `src/bin/pathlint/run.rs` — the library is shaped so the binary
//! is a thin orchestration on top of the same primitives.
// Internal modules — `#[doc(hidden)] pub` for the ones the
// `pathlint` binary in `src/bin/pathlint/` needs to call across
// the lib/bin boundary, `pub(crate)` for everything strictly
// internal to the lib. Neither tier appears on docs.rs and
// neither is part of the library contract.
//
// 0.0.17 moved `cli` and `run` out of the lib entirely; they now
// live in `src/bin/pathlint/` alongside `main.rs`. The binary
// still consumes some lib internals (formatter, presentation,
// init template, OS PATH reader, command resolver) — those stay
// `#[doc(hidden)] pub` so Cargo can route the call across the
// crate boundary without putting them on the public surface.
pub
/// Shared dependency carrier embedded in every per-function `*Deps`
/// (see [`doctor::AnalyzeDeps`], [`lint::EvaluateDeps`],
/// [`trace::LocateDeps`], [`sort::SortDeps`]).
///
/// Today this only holds `env_lookup`, but the four
/// per-function carriers all reach for the same env oracle, so
/// concentrating it here keeps the "how does pathlint read the
/// process env?" decision in a single place. Future cross-cutting
/// concerns (logging hook, deterministic clock, etc.) belong here
/// too.
///
/// Production callers usually start from [`CommonDeps::production`]
/// and pass the resulting value into one of the per-function
/// carriers. Embedders supplying a deterministic env oracle
/// construct the struct directly.
///
/// 0.0.27+.
/// `Fn(&str) -> Option<String>` env oracle, type-erased through a
/// trait object. Used as the type of `CommonDeps::env_lookup` and
/// (transitively) every per-function `*Deps`. The `'a` lifetime lets
/// the closure borrow from the test stack; production wiring uses
/// `'static`. 0.0.27+.
pub type EnvLookupFn<'a> = ;
/// Per-entry cross-source carrier. Wraps a single-source
/// [`crate::path_entry::PathEntry`] together with an optional
/// `provenance_raw`: the `raw` form observed at *another* source
/// whose `expanded` matches this one. The only place that sets
/// `provenance_raw` today is
/// [`crate::path_source::reconcile_process_with_registry`], which
/// recovers the registry `%VAR%` form for entries the OS expanded
/// on its way into `getenv("PATH")` (the `--target process` overlay,
/// see ADR-0004).
///
/// 0.0.28 split this out of `PathEntry` so the carrier inside the
/// lib has one clean concept per type: `PathEntry` is "what one
/// source said this entry is"; `Attribution` is "that observation
/// plus whatever cross-source hint we have". See ADR-0008.