Skip to main content

soothfast_report/
llms.rs

1//! `llms.txt`: a compact machine-readable digest of the public surface and
2//! its measured costs, for AI agents. Guaranteed current because it is
3//! derived — the answer to "agents reading stale READMEs as truth".
4
5use std::collections::BTreeMap;
6
7use serde_json::Value;
8
9/// One public item as fed by the CLI (from the docs-engine surface).
10pub struct SurfaceEntry {
11    pub path: String,
12    pub kind: String,
13    pub signature: String,
14    /// Name of the crate this item was extracted from (groups multi-crate
15    /// digests; report.rs concatenates one `surface_entries()` call per
16    /// `-p PKG` flag into a single entry list).
17    pub pkg: String,
18    /// First line of the doc comment — compact, for search hits and lists.
19    pub summary: String,
20    /// Full doc comment (all paragraphs, code fences intact) — the whole
21    /// point of `llms.txt` over a bare signature list: an agent gets the
22    /// actual usage explanation, not just a name to guess at.
23    pub docs: String,
24}
25
26/// Render `llms.txt`: every public item grouped by crate, with its full
27/// doc comment and measured facts attributed via `covers` (or direct id)
28/// matching.
29pub fn render(pkg: &str, entries: &[SurfaceEntry], baseline: &Value) -> String {
30    // Measurements attribute to public items via `covers` (or direct id).
31    // These numbers are point-in-time and will drift slightly between any
32    // two measurement runs (even perfcnt/alloc, not just walltime) — a
33    // committed llms.txt's drift gate (see the Makefile's `llms-gate`)
34    // deliberately ignores `**Measured:**` lines rather than pretend these
35    // are reproducible; regression detection is `make gate`'s job.
36    let mut measured: BTreeMap<String, String> = BTreeMap::new();
37    if let Some(items) = baseline["items"].as_object() {
38        for (id, m) in items {
39            let target = m["covers"].as_str().filter(|c| !c.is_empty()).unwrap_or(id);
40            let mut facts = Vec::new();
41            if let Some(i) = m["perfcnt"]["instructions"].as_u64() {
42                facts.push(format!("{i} instructions/iter"));
43            }
44            if let Some(ns) = m["walltime"]["median_ns"].as_f64() {
45                facts.push(format!("{ns:.0}ns median"));
46            }
47            if let Some(a) = m["alloc"]["allocs"].as_u64() {
48                facts.push(format!("{a} allocs/iter"));
49            }
50            if let Some(p) = m["asyncexec"]["polls"].as_u64() {
51                facts.push(format!("{p} polls/iter"));
52            }
53            if !facts.is_empty() {
54                measured.insert(target.to_string(), facts.join(", "));
55            }
56        }
57    }
58
59    // Group by source crate, preserving each crate's own path order.
60    let mut groups: BTreeMap<&str, Vec<&SurfaceEntry>> = BTreeMap::new();
61    for e in entries {
62        if e.kind == "module" {
63            continue;
64        }
65        groups.entry(e.pkg.as_str()).or_default().push(e);
66    }
67    // A crate-name heading is only informative once there's more than one
68    // crate in the digest — a single-crate render already says so in the H1.
69    let multi_crate = groups.len() > 1;
70    let item_level = if multi_crate { "####" } else { "###" };
71
72    let mut out = format!(
73        "# {pkg}\n\n> Generated by `cargo soothfast report`, derived from the current code and\n> measurements rather than hand-written. Trust it over any prose.\n\n## Public API\n\n"
74    );
75    for (crate_name, items) in &groups {
76        if multi_crate {
77            out.push_str(&format!("### {crate_name}\n\n"));
78        }
79        for e in items {
80            let head = if e.signature.is_empty() {
81                e.path.as_str()
82            } else {
83                e.signature.as_str()
84            };
85            out.push_str(&format!("{item_level} `{head}`\n\n"));
86            let doc = e.docs.trim();
87            if !doc.is_empty() {
88                out.push_str(&nest_headings(doc, item_level.len()));
89                out.push_str("\n\n");
90            }
91            if let Some(facts) = measured.get(&e.path) {
92                out.push_str(&format!("**Measured:** {facts}\n\n"));
93            }
94        }
95    }
96    out
97}
98
99/// Rustdoc conventions (`# Panics`, `# Examples`, ...) write ATX headings
100/// starting at depth 1, since a doc comment has no enclosing document. Once
101/// spliced under an item heading here, those need to nest *below* it rather
102/// than jump back to top level — shift every heading outside a fenced code
103/// block down by `item_depth`, capped at markdown's depth-6 ceiling.
104fn nest_headings(text: &str, item_depth: usize) -> String {
105    let mut in_fence = false;
106    text.lines()
107        .map(|line| {
108            if line.trim_start().starts_with("```") {
109                in_fence = !in_fence;
110                return line.to_string();
111            }
112            if in_fence {
113                return line.to_string();
114            }
115            let hashes = line.chars().take_while(|&c| c == '#').count();
116            if hashes > 0 && hashes <= 6 && line.as_bytes().get(hashes) == Some(&b' ') {
117                let depth = (item_depth + hashes).min(6);
118                format!("{}{}", "#".repeat(depth), &line[hashes..])
119            } else {
120                line.to_string()
121            }
122        })
123        .collect::<Vec<_>>()
124        .join("\n")
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde_json::json;
131
132    fn entry(pkg: &str, path: &str, docs: &str) -> SurfaceEntry {
133        SurfaceEntry {
134            path: path.to_string(),
135            kind: "function".to_string(),
136            signature: format!("pub fn {path}()"),
137            pkg: pkg.to_string(),
138            summary: docs.lines().next().unwrap_or("").to_string(),
139            docs: docs.to_string(),
140        }
141    }
142
143    #[test]
144    fn nest_headings_shifts_rustdoc_sections_below_the_item_heading() {
145        let doc = "Does a thing.\n\n# Panics\n\nIf you misuse it.";
146        assert_eq!(
147            nest_headings(doc, 4),
148            "Does a thing.\n\n##### Panics\n\nIf you misuse it."
149        );
150    }
151
152    #[test]
153    fn nest_headings_ignores_hashes_inside_fenced_code() {
154        let doc = "Usage:\n\n```ignore\n# not a heading\n#[attr]\nfn f() {}\n```\n\n# Examples";
155        let shifted = nest_headings(doc, 3);
156        assert!(shifted.contains("```ignore\n# not a heading\n#[attr]"));
157        assert!(shifted.contains("#### Examples"));
158    }
159
160    #[test]
161    fn nest_headings_caps_at_depth_six() {
162        assert_eq!(nest_headings("###### already max", 4), "###### already max");
163    }
164
165    #[test]
166    fn render_groups_multiple_crates_under_their_own_heading() {
167        let entries = vec![
168            entry("soothfast", "soothfast::keep", "Optimizer barrier."),
169            entry(
170                "soothfast-macros",
171                "soothfast_macros::bench",
172                "Register a bench.",
173            ),
174        ];
175        let out = render("soothfast, soothfast-macros", &entries, &json!({}));
176        assert!(out.contains("### soothfast\n"));
177        assert!(out.contains("### soothfast-macros\n"));
178        assert!(out.contains("#### `pub fn soothfast::keep()`"));
179    }
180
181    #[test]
182    fn render_single_crate_omits_the_redundant_crate_heading() {
183        let entries = vec![entry("soothfast", "soothfast::keep", "Optimizer barrier.")];
184        let out = render("soothfast", &entries, &json!({}));
185        assert!(!out.contains("### soothfast\n"));
186        assert!(out.contains("### `pub fn soothfast::keep()`"));
187    }
188
189    #[test]
190    fn render_attaches_measured_facts_by_covers_or_id() {
191        let entries = vec![entry("soothfast", "soothfast::keep", "Optimizer barrier.")];
192        let baseline = json!({
193            "items": {
194                "soothfast::keep": {
195                    "perfcnt": { "instructions": 42 },
196                    "walltime": { "median_ns": 12.0 },
197                }
198            }
199        });
200        let out = render("soothfast", &entries, &baseline);
201        assert!(out.contains("**Measured:** 42 instructions/iter, 12ns median"));
202    }
203
204    #[test]
205    fn render_skips_module_kind_entries() {
206        let mut e = entry("soothfast", "soothfast::registry", "");
207        e.kind = "module".to_string();
208        let out = render("soothfast", &[e], &json!({}));
209        assert!(!out.contains("registry"));
210    }
211}