Skip to main content

provable_contracts/ontology/extract/
example.rs

1//! aprender#3560 R1 — `extract:example`: every cargo example target in the workspace becomes an `ont:Example`.
2//!
3//! The focus objects are `<member>/examples/*.rs` and `<member>/examples/*/main.rs` — cargo's two auto-discovered
4//! example forms — of every workspace MEMBER, found by the same `[workspace] members`/`exclude` reading
5//! [`super::code`] uses. An `examples/` dir under a manifest the workspace excludes is counted
6//! (`non_member_examples`) and not extracted: cargo will not build it, so it is not an example of this workspace.
7//! Helper modules below `examples/<x>/` other than `main.rs` are not targets.
8//!
9//! Each example carries `example:file`, `example:crate` (the `[package] name`), `example:name`, one
10//! `example:namesModel` per model family its text names (the [`FAMILIES`] table: a token scan, so `graphics` is not
11//! `phi` and `llama.cpp` is a runtime, not a model), and `example:namesQwen35`.
12//!
13//! Currency (R4): the current model family is [`CURRENT_FAMILY`]. An example is `example:modelCurrent` when it names no
14//! model family, names the current one, or carries a `// ont:model-pinned: <reason>` line saying why an older model is
15//! the point (a parity fixture, a tiny test model) — the reason is kept as `example:modelPinned`. The stale count is
16//! pinned shrink-only ([`STALE_EXAMPLES_PINNED`]): a new example on an old model is RED, a migrated one lowers the pin.
17//!
18//! Vacuity (the issue's acceptance 1): at a `[workspace]` root, reading ZERO examples is an extractor error — the
19//! shapes gate counts it as a violation (PV-ONT-012), never a green over an empty corpus. A fixture corpus with no
20//! workspace manifest is not measured and is not an error.
21//!
22//! IRI: `https://ont.paiml.dev/v1alpha1/example/<repo-relative file>`. No blank nodes; byte-ordered walk (R-15).
23
24use std::collections::{BTreeMap, BTreeSet};
25use std::path::{Path, PathBuf};
26
27use super::code::{manifest_names, workspace_membership};
28use super::gguf::ExtractError;
29use crate::ontology::rdf::{iri_path, ont, Graph, Term, PROV_ENTITY, RDF_TYPE};
30
31/// An `example:*` vocabulary term.
32#[must_use]
33pub fn ex(name: &str) -> String {
34    ont(&format!("example/{name}"))
35}
36
37/// The model families a token scan recognises, each with the token prefixes that name it. Order matters: the first
38/// family whose prefix starts a token wins, so `qwen3.5` is tried before `qwen3` and `tinyllama` before `llama`.
39pub const FAMILIES: &[(&str, &[&str])] = &[
40    ("qwen3.5", &["qwen3.5", "qwen3_5", "qwen3-5", "qwen35"]),
41    ("qwen3", &["qwen3"]),
42    ("qwen2.5", &["qwen2.5", "qwen2_5", "qwen2-5", "qwen25"]),
43    ("qwen2", &["qwen2"]),
44    ("tinyllama", &["tinyllama"]),
45    ("llama", &["llama"]),
46    ("mistral", &["mistral"]),
47    ("phi", &["phi-", "phi_", "phi2", "phi3", "phi4"]),
48    ("gemma", &["gemma"]),
49    ("smollm", &["smollm"]),
50    ("gpt2", &["gpt2", "gpt-2"]),
51    ("deepseek", &["deepseek"]),
52];
53
54/// The model family every example that names a model is expected to name (#3560).
55pub const CURRENT_FAMILY: &str = "qwen3.5";
56
57/// Examples on this tree that name a model family and neither name [`CURRENT_FAMILY`] nor declare a pin — measured
58/// 2026-09-24 on B3 (430 of the 432 that name a model). Shrink-only: lower it as examples migrate or pin; never raise it.
59pub const STALE_EXAMPLES_PINNED: usize = 430;
60
61/// The marker that pins an example to an older model, followed by the reason.
62pub const PIN_MARKER: &str = "ont:model-pinned:";
63
64/// The reason an example gives for naming an older model, if it gives one.
65#[must_use]
66pub fn pin_reason(text: &str) -> Option<String> {
67    text.lines()
68        .filter_map(|l| l.trim_start().strip_prefix("//"))
69        .filter_map(|l| {
70            l.trim_start_matches(['/', '!'])
71                .trim()
72                .strip_prefix(PIN_MARKER)
73        })
74        .map(str::trim)
75        .find(|r| !r.is_empty())
76        .map(str::to_string)
77}
78
79/// Tokens that start like a family and are not one: the llama.cpp runtime.
80const NOT_A_MODEL: &[&str] = &["llama.cpp", "llama_cpp", "llama-cpp", "llamacpp"];
81
82/// The model families `text` names, sorted.
83#[must_use]
84pub fn families_in(text: &str) -> BTreeSet<&'static str> {
85    let lower = text.to_lowercase();
86    lower
87        .split(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
88        .filter(|t| !NOT_A_MODEL.iter().any(|n| t.starts_with(n)))
89        .filter_map(|t| {
90            if t == "phi" {
91                return Some("phi");
92            }
93            FAMILIES
94                .iter()
95                .find(|(_, prefixes)| prefixes.iter().any(|p| t.starts_with(p)))
96                .map(|(f, _)| *f)
97        })
98        .collect()
99}
100
101/// One example target.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Example {
104    /// Repo-relative file.
105    pub file: String,
106    pub krate: String,
107    pub name: String,
108    pub families: BTreeSet<&'static str>,
109    /// The `ont:model-pinned:` reason, when the example gives one.
110    pub pinned: Option<String>,
111}
112
113impl Example {
114    /// Names no model, names the current one, or says why not.
115    #[must_use]
116    pub fn model_current(&self) -> bool {
117        self.families.is_empty() || self.families.contains(CURRENT_FAMILY) || self.pinned.is_some()
118    }
119}
120
121/// Counts reported beside the graph.
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
123pub struct ExampleStats {
124    /// The repo root carries a `[workspace]` manifest — the only case in which zero examples can be an error.
125    pub at_workspace_root: bool,
126    /// Member packages read, with or without example targets.
127    pub members: usize,
128    /// Member packages with at least one example target.
129    pub packages: usize,
130    pub examples: usize,
131    /// Example targets under a manifest the workspace does not admit (not extracted).
132    pub non_member_examples: usize,
133    pub naming_a_model: usize,
134    pub naming_qwen35: usize,
135    /// Examples naming a model that is neither current nor pinned — the number [`STALE_EXAMPLES_PINNED`] bounds.
136    pub stale: usize,
137    /// Examples that declare `ont:model-pinned:`.
138    pub pinned: usize,
139    /// `family -> examples naming it`.
140    pub by_family: BTreeMap<String, usize>,
141    pub errors: Vec<ExtractError>,
142}
143
144/// The example target files of the package in `dir`, byte-ordered: `examples/*.rs` and `examples/*/main.rs`.
145#[must_use]
146pub fn targets_of(dir: &Path) -> Vec<PathBuf> {
147    let Ok(rd) = std::fs::read_dir(dir.join("examples")) else {
148        return Vec::new();
149    };
150    let mut out: Vec<PathBuf> = rd
151        .flatten()
152        .map(|e| e.path())
153        .filter_map(|p| {
154            if p.is_dir() {
155                Some(p.join("main.rs")).filter(|m| m.is_file())
156            } else {
157                Some(p).filter(|p| p.extension().is_some_and(|e| e == "rs"))
158            }
159        })
160        .collect();
161    out.sort();
162    out
163}
164
165/// Every `Cargo.toml` under `root`, skipping build and vcs dirs, byte-ordered.
166pub(crate) fn manifests(root: &Path) -> Vec<PathBuf> {
167    let mut out = Vec::new();
168    let mut stack = vec![root.to_path_buf()];
169    while let Some(dir) = stack.pop() {
170        let Ok(rd) = std::fs::read_dir(&dir) else {
171            continue;
172        };
173        for p in rd.flatten().map(|e| e.path()) {
174            let name = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();
175            if p.is_dir() {
176                if !matches!(name, "target" | ".git" | ".lake" | "node_modules") {
177                    stack.push(p);
178                }
179            } else if name == "Cargo.toml" {
180                out.push(p);
181            }
182        }
183    }
184    out.sort();
185    out
186}
187
188fn rel(root: &Path, p: &Path) -> String {
189    p.strip_prefix(root)
190        .unwrap_or(p)
191        .to_string_lossy()
192        .replace('\\', "/")
193}
194
195/// The name cargo gives the target: the file stem, or the directory for `examples/<name>/main.rs`.
196fn target_name(file: &Path) -> String {
197    let stem = file.file_stem().unwrap_or_default();
198    let name = if stem == "main" {
199        file.parent().and_then(Path::file_name).unwrap_or(stem)
200    } else {
201        stem
202    };
203    name.to_string_lossy().to_string()
204}
205
206/// Walk the workspace under `root`: the member examples, and the counts.
207#[must_use]
208pub fn walk(root: &Path) -> (Vec<Example>, ExampleStats) {
209    let membership = std::fs::read_to_string(root.join("Cargo.toml"))
210        .ok()
211        .and_then(|t| workspace_membership(&t));
212    let mut stats = ExampleStats {
213        at_workspace_root: membership.is_some(),
214        ..ExampleStats::default()
215    };
216    let mut out = Vec::new();
217    // Without a `[workspace]` table cargo builds only the root package; a manifest below it is another project. So the
218    // walk does not descend: a contract dir whose parent is a shared scratch dir would otherwise admit every package
219    // other jobs leave there, and two extractions of the same corpus would differ (T34).
220    let candidates = if membership.is_some() {
221        manifests(root)
222    } else {
223        Some(root.join("Cargo.toml"))
224            .filter(|m| m.is_file())
225            .into_iter()
226            .collect()
227    };
228    for manifest in candidates {
229        let dir = manifest.parent().unwrap_or(root);
230        let package = std::fs::read_to_string(&manifest)
231            .ok()
232            .and_then(|t| manifest_names(&t).package);
233        let admitted = membership.as_ref().is_none_or(|m| m.admits(root, dir));
234        stats.members += usize::from(admitted && package.is_some());
235        let targets = targets_of(dir);
236        if targets.is_empty() {
237            continue;
238        }
239        let Some(krate) = package.filter(|_| admitted) else {
240            stats.non_member_examples += targets.len();
241            continue;
242        };
243        stats.packages += 1;
244        for file in targets {
245            let Ok(text) = std::fs::read_to_string(&file) else {
246                stats.errors.push(ExtractError {
247                    file: rel(root, &file),
248                    what: "example target is not readable UTF-8".into(),
249                });
250                continue;
251            };
252            out.push(Example {
253                file: rel(root, &file),
254                krate: krate.clone(),
255                name: target_name(&file),
256                families: families_in(&text),
257                pinned: pin_reason(&text),
258            });
259        }
260    }
261    stats.examples = out.len();
262    for e in &out {
263        stats.naming_a_model += usize::from(!e.families.is_empty());
264        stats.naming_qwen35 += usize::from(e.families.contains(CURRENT_FAMILY));
265        stats.stale += usize::from(!e.model_current());
266        stats.pinned += usize::from(e.pinned.is_some());
267        for f in &e.families {
268            *stats.by_family.entry((*f).to_string()).or_default() += 1;
269        }
270    }
271    // Zero examples is a measurement when the workspace simply has none. It is RED when the walk read nothing it
272    // could have: no member admitted, or example targets on disk and every one refused by the membership reading.
273    if stats.at_workspace_root
274        && stats.examples == 0
275        && (stats.members == 0 || stats.non_member_examples > 0)
276    {
277        stats.errors.push(ExtractError {
278            file: "Cargo.toml".into(),
279            what: format!(
280                "a [workspace] root read zero example targets from {} member(s) while {} target(s) sat outside \
281                 the membership — the walk measured nothing",
282                stats.members, stats.non_member_examples
283            ),
284        });
285    }
286    (out, stats)
287}
288
289/// One example into `g`.
290pub fn emit(g: &mut Graph, e: &Example) {
291    let n = iri_path("example", &e.file.split('/').collect::<Vec<_>>());
292    g.insert(n.clone(), RDF_TYPE, Term::iri(ont("Example")));
293    g.insert(n.clone(), RDF_TYPE, Term::iri(PROV_ENTITY));
294    g.insert(n.clone(), ex("file"), Term::string(&e.file));
295    g.insert(n.clone(), ex("crate"), Term::string(&e.krate));
296    g.insert(n.clone(), ex("name"), Term::string(&e.name));
297    for f in &e.families {
298        g.insert(n.clone(), ex("namesModel"), Term::string(*f));
299    }
300    g.insert(
301        n.clone(),
302        ex("namesQwen35"),
303        Term::boolean(e.families.contains(CURRENT_FAMILY)),
304    );
305    g.insert(
306        n.clone(),
307        ex("modelCurrent"),
308        Term::boolean(e.model_current()),
309    );
310    if let Some(r) = &e.pinned {
311        g.insert(n.clone(), ex("modelPinned"), Term::string(r));
312    }
313}
314
315/// The examples of the workspace under `contract_dir`'s parent, into `g`.
316pub fn extract(contract_dir: &Path, g: &mut Graph) -> ExampleStats {
317    let root = super::repo_root(contract_dir);
318    let (examples, stats) = walk(&root);
319    for e in &examples {
320        emit(g, e);
321    }
322    stats
323}
324
325/// The scan tells a model from its look-alikes, this run: a Qwen3.5 size and a phi are named; the llama.cpp
326/// runtime and the word `graphics` are not; and an example on Qwen2.5 whose comment is not a pin is stale.
327#[must_use]
328pub fn positive_control() -> bool {
329    let named = families_in("load Qwen3.5-4B, compare to phi-3 on graphics via llama.cpp");
330    let expected: BTreeSet<&str> = ["qwen3.5", "phi"].into_iter().collect();
331    let old = Example {
332        file: String::new(),
333        krate: String::new(),
334        name: String::new(),
335        families: families_in("Qwen2.5-Coder"),
336        pinned: pin_reason("// qwen2.5 on purpose"),
337    };
338    named == expected
339        && families_in("Qwen3-8B").contains("qwen3")
340        && !families_in("Qwen3-8B").contains("qwen3.5")
341        && !old.model_current()
342}
343
344#[cfg(test)]
345#[path = "example_tests.rs"]
346mod tests;