provable_contracts/ontology/extract/
example.rs1use 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#[must_use]
33pub fn ex(name: &str) -> String {
34 ont(&format!("example/{name}"))
35}
36
37pub 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
54pub const CURRENT_FAMILY: &str = "qwen3.5";
56
57pub const STALE_EXAMPLES_PINNED: usize = 430;
60
61pub const PIN_MARKER: &str = "ont:model-pinned:";
63
64#[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
79const NOT_A_MODEL: &[&str] = &["llama.cpp", "llama_cpp", "llama-cpp", "llamacpp"];
81
82#[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#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Example {
104 pub file: String,
106 pub krate: String,
107 pub name: String,
108 pub families: BTreeSet<&'static str>,
109 pub pinned: Option<String>,
111}
112
113impl Example {
114 #[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#[derive(Debug, Clone, Default, PartialEq, Eq)]
123pub struct ExampleStats {
124 pub at_workspace_root: bool,
126 pub members: usize,
128 pub packages: usize,
130 pub examples: usize,
131 pub non_member_examples: usize,
133 pub naming_a_model: usize,
134 pub naming_qwen35: usize,
135 pub stale: usize,
137 pub pinned: usize,
139 pub by_family: BTreeMap<String, usize>,
141 pub errors: Vec<ExtractError>,
142}
143
144#[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
165pub(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
195fn 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#[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 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 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
289pub 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
315pub 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#[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;