Skip to main content

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}