mandible_core/provenance.rs
1//! Per-item provenance: which extraction source(s) contributed a
2//! [`crate::CommandNode`], [`crate::Flag`], or [`crate::Positional`], and the
3//! two-axis [`Authority`] each source carries for merge decisions.
4//!
5//! See spec §4.2 and §4.4. Provenance lives on each item individually —
6//! never as one badge for a whole tree — because after a multi-tier merge a
7//! node's own fields and its children's fields may legitimately come from
8//! different sources, and a single node-level badge covering both would lie
9//! about the children.
10
11use serde::{Deserialize, Serialize};
12use smallvec::SmallVec;
13
14/// Which extraction source(s) contributed to an item, in contribution order,
15/// plus a confidence score set only by heuristic tiers.
16#[derive(Debug, Clone, PartialEq, Default)]
17pub struct Provenance {
18 /// Contributing sources, ordered by contribution (earliest first).
19 pub sources: SmallVec<[Source; 2]>,
20 /// Set only when a heuristic tier (e.g. Tier B help-text) produced this
21 /// item; `None` for structured/authoritative sources.
22 pub confidence: Option<f32>,
23}
24
25/// Serde representation of [`Provenance`], used instead of deriving
26/// `Serialize`/`Deserialize` directly on the struct: `smallvec`'s own
27/// `serde` feature currently miscompiles against the split `serde_core`
28/// crate introduced in `serde` 1.0.229 (a lifetime error at the derive
29/// site). Serializing through a plain `Vec` sidesteps that entirely and
30/// keeps the on-the-wire shape identical (`{"sources": [...], "confidence":
31/// ...}`).
32#[derive(Serialize, Deserialize)]
33struct ProvenanceRepr {
34 sources: Vec<Source>,
35 confidence: Option<f32>,
36}
37
38impl Serialize for Provenance {
39 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
40 where
41 S: serde::Serializer,
42 {
43 ProvenanceRepr {
44 sources: self.sources.to_vec(),
45 confidence: self.confidence,
46 }
47 .serialize(serializer)
48 }
49}
50
51impl<'de> Deserialize<'de> for Provenance {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: serde::Deserializer<'de>,
55 {
56 let repr = ProvenanceRepr::deserialize(deserializer)?;
57 Ok(Provenance {
58 sources: SmallVec::from_vec(repr.sources),
59 confidence: repr.confidence,
60 })
61 }
62}
63
64impl Provenance {
65 /// A `Provenance` crediting a single source, with no confidence score.
66 pub fn single(source: Source) -> Provenance {
67 let mut sources = SmallVec::new();
68 sources.push(source);
69 Provenance {
70 sources,
71 confidence: None,
72 }
73 }
74
75 /// A `Provenance` crediting a single heuristic source with a confidence
76 /// score.
77 pub fn with_confidence(source: Source, confidence: f32) -> Provenance {
78 let mut sources = SmallVec::new();
79 sources.push(source);
80 Provenance {
81 sources,
82 confidence: Some(confidence),
83 }
84 }
85
86 /// The highest authority on `axis` among this item's contributing
87 /// sources. `0` if there are no contributing sources.
88 pub fn effective_authority(&self, axis: Axis) -> u8 {
89 self.sources
90 .iter()
91 .map(|s| s.authority().on(axis))
92 .max()
93 .unwrap_or(0)
94 }
95
96 /// Merge another `Provenance` into this one: union the source lists
97 /// (deduplicated, order preserved) and combine confidence
98 /// conservatively (the lower of the two, since overall trust is bounded
99 /// by the least-confident contributor).
100 pub fn absorb(&mut self, other: &Provenance) {
101 for s in &other.sources {
102 if !self.sources.contains(s) {
103 self.sources.push(s.clone());
104 }
105 }
106 self.confidence = match (self.confidence, other.confidence) {
107 (Some(a), Some(b)) => Some(a.min(b)),
108 (Some(a), None) => Some(a),
109 (None, Some(b)) => Some(b),
110 (None, None) => None,
111 };
112 }
113}
114
115/// One axis of [`Authority`]: structural facts (names, nesting, arity, which
116/// flags exist) vs. prose (descriptions, summaries, examples).
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum Axis {
119 /// Trust for names, nesting, arity, which flags exist.
120 Structural,
121 /// Trust for descriptions, summaries, examples.
122 Prose,
123}
124
125/// The two-axis trust level a [`Source`] carries. See spec §4.4's authority
126/// table — the tier with the best structure is frequently not the tier with
127/// the best prose, so merge resolves each axis independently rather than by
128/// a single priority order.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct Authority {
131 /// Trust for names, nesting, arity, which flags exist.
132 pub structural: u8,
133 /// Trust for descriptions, summaries, examples.
134 pub prose: u8,
135}
136
137impl Authority {
138 /// The authority value for a given axis.
139 pub fn on(&self, axis: Axis) -> u8 {
140 match axis {
141 Axis::Structural => self.structural,
142 Axis::Prose => self.prose,
143 }
144 }
145}
146
147/// The origin of a piece of extracted data. See spec §4.2 and §7.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub enum Source {
150 /// A native, version-accurate dynamic probe (Tier E): cobra
151 /// `__complete`, clap `CompleteEnv`, argcomplete.
152 NativeDynamic {
153 /// e.g. `"cobra-dunder-complete"`, `"clap-complete-env"`.
154 ///
155 /// `String` rather than `&'static str`: a `Source` must round-trip
156 /// through the on-disk cache (spec §11), and a borrowed `'static`
157 /// string cannot in general be produced by `Deserialize` without
158 /// leaking memory.
159 protocol: String,
160 },
161 /// A vendored or live structured catalog (Tier A).
162 KnownSpec {
163 /// e.g. `"carapace"`, `"withfig"`.
164 provider: String,
165 },
166 /// Structural parsing of a generated shell completion script (Tier C).
167 CompletionScript {
168 /// e.g. `"zsh"`, `"bash"`.
169 shell: String,
170 },
171 /// Man page extraction (Tier D).
172 ManPage {
173 /// Whether the page used semantic `mdoc(7)` macros or plain `man(7)`.
174 format: ManFormat,
175 },
176 /// `--help`/`-h`/`help` grammar parsing (Tier B).
177 HelpText,
178 /// A user-local override file (Tier F).
179 UserOverride,
180}
181
182/// Which man page macro package produced a [`Source::ManPage`] item.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184pub enum ManFormat {
185 /// Semantic macros (`.Fl`, `.Ar`, `.Nm`) — real structure, not inference.
186 Mdoc,
187 /// Typeset prose with weak semantic tagging.
188 Man,
189}
190
191impl Source {
192 /// This source's two-axis [`Authority`], per spec §4.4's table.
193 pub fn authority(&self) -> Authority {
194 match self {
195 Source::UserOverride => Authority {
196 structural: 255,
197 prose: 255,
198 },
199 Source::NativeDynamic { .. } => Authority {
200 structural: 200,
201 prose: 40,
202 },
203 Source::CompletionScript { .. } => Authority {
204 structural: 150,
205 prose: 30,
206 },
207 Source::KnownSpec { .. } => Authority {
208 structural: 120,
209 prose: 200,
210 },
211 Source::ManPage { .. } => Authority {
212 structural: 60,
213 prose: 180,
214 },
215 Source::HelpText => Authority {
216 structural: 80,
217 prose: 120,
218 },
219 }
220 }
221
222 /// A short, human-readable label for UI footers, e.g. `"carapace"`,
223 /// `"help-text"`.
224 pub fn label(&self) -> String {
225 match self {
226 Source::NativeDynamic { protocol } => protocol.to_string(),
227 Source::KnownSpec { provider } => provider.to_string(),
228 Source::CompletionScript { shell } => format!("completion-{shell}"),
229 Source::ManPage {
230 format: ManFormat::Mdoc,
231 } => "man(mdoc)".to_string(),
232 Source::ManPage {
233 format: ManFormat::Man,
234 } => "man".to_string(),
235 Source::HelpText => "help-text".to_string(),
236 Source::UserOverride => "override".to_string(),
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn authority_table_matches_spec() {
247 assert_eq!(
248 Source::UserOverride.authority(),
249 Authority {
250 structural: 255,
251 prose: 255
252 }
253 );
254 assert_eq!(
255 Source::NativeDynamic {
256 protocol: "x".to_string()
257 }
258 .authority(),
259 Authority {
260 structural: 200,
261 prose: 40
262 }
263 );
264 assert_eq!(
265 Source::CompletionScript {
266 shell: "zsh".to_string()
267 }
268 .authority(),
269 Authority {
270 structural: 150,
271 prose: 30
272 }
273 );
274 assert_eq!(
275 Source::KnownSpec {
276 provider: "carapace".to_string()
277 }
278 .authority(),
279 Authority {
280 structural: 120,
281 prose: 200
282 }
283 );
284 assert_eq!(
285 Source::ManPage {
286 format: ManFormat::Mdoc
287 }
288 .authority(),
289 Authority {
290 structural: 60,
291 prose: 180
292 }
293 );
294 assert_eq!(
295 Source::HelpText.authority(),
296 Authority {
297 structural: 80,
298 prose: 120
299 }
300 );
301 }
302
303 #[test]
304 fn effective_authority_is_max_over_sources() {
305 let p = Provenance {
306 sources: SmallVec::from_vec(vec![
307 Source::HelpText,
308 Source::KnownSpec {
309 provider: "carapace".to_string(),
310 },
311 ]),
312 confidence: None,
313 };
314 assert_eq!(p.effective_authority(Axis::Prose), 200);
315 assert_eq!(p.effective_authority(Axis::Structural), 120);
316 }
317
318 #[test]
319 fn absorb_dedups_sources() {
320 let mut a = Provenance::single(Source::KnownSpec {
321 provider: "carapace".to_string(),
322 });
323 let b = Provenance::single(Source::KnownSpec {
324 provider: "carapace".to_string(),
325 });
326 a.absorb(&b);
327 assert_eq!(a.sources.len(), 1);
328 }
329
330 #[test]
331 fn absorb_takes_min_confidence() {
332 let mut a = Provenance::with_confidence(Source::HelpText, 0.9);
333 let b = Provenance::with_confidence(Source::HelpText, 0.4);
334 a.absorb(&b);
335 assert_eq!(a.confidence, Some(0.4));
336 }
337}