Skip to main content

lemma/documentation/
mod.rs

1//! Embedded language guides and example specs.
2
3macro_rules! doc {
4    ($path:literal) => {
5        concat!(env!("CARGO_MANIFEST_DIR"), $path)
6    };
7}
8
9pub const LLMS_TXT: &str = concat!(
10    include_str!(doc!("/documentation/guide/00_intro.md")),
11    "\n\n---\n\n",
12    include_str!(doc!("/documentation/guide/05_method.md")),
13    "\n\n---\n\n",
14    include_str!(doc!("/documentation/guide/10_syntax.md")),
15    "\n\n---\n\n",
16    include_str!(doc!("/documentation/guide/20_composition.md")),
17    "\n\n---\n\n",
18    include_str!(doc!("/documentation/guide/30_data.md")),
19    "\n\n---\n\n",
20    include_str!(doc!("/documentation/guide/40_units.md")),
21    "\n\n---\n\n",
22    include_str!(doc!("/documentation/guide/50_rules.md")),
23    "\n\n---\n\n",
24    include_str!(doc!("/documentation/guide/60_veto.md")),
25    "\n\n---\n\n",
26    include_str!(doc!("/documentation/guide/70_anti_patterns.md")),
27    "\n\n---\n\n",
28    include_str!(doc!("/documentation/guide/80_footer.md")),
29);
30
31pub const EVALUATE_GUIDE: &str = include_str!(doc!("/documentation/evaluate_guide.md"));
32
33const METHOD: &str = include_str!(doc!("/documentation/guide/05_method.md"));
34const SYNTAX: &str = include_str!(doc!("/documentation/guide/10_syntax.md"));
35const COMPOSITION: &str = include_str!(doc!("/documentation/guide/20_composition.md"));
36const DATA: &str = include_str!(doc!("/documentation/guide/30_data.md"));
37const UNITS: &str = include_str!(doc!("/documentation/guide/40_units.md"));
38const RULES: &str = include_str!(doc!("/documentation/guide/50_rules.md"));
39const VETO: &str = include_str!(doc!("/documentation/guide/60_veto.md"));
40const ANTI_PATTERNS: &str = include_str!(doc!("/documentation/guide/70_anti_patterns.md"));
41
42pub const EXAMPLE_01_COFFEE_ORDER: &str =
43    include_str!(doc!("/documentation/examples/01_coffee_order.lemma"));
44pub const EXAMPLE_02_LIBRARY_FEES: &str =
45    include_str!(doc!("/documentation/examples/02_library_fees.lemma"));
46pub const EXAMPLE_03_RECIPE_SCALING: &str =
47    include_str!(doc!("/documentation/examples/03_recipe_scaling.lemma"));
48pub const EXAMPLE_04_MEMBERSHIP_BENEFITS: &str =
49    include_str!(doc!("/documentation/examples/04_membership_benefits.lemma"));
50pub const EXAMPLE_05_WEATHER_CLOTHING: &str =
51    include_str!(doc!("/documentation/examples/05_weather_clothing.lemma"));
52pub const EXAMPLE_NL_TAX_NET_SALARY: &str =
53    include_str!(doc!("/documentation/examples/nl/tax/net_salary.lemma"));
54
55/// Guide topics: authoring sections under `documentation/guide/`,
56/// plus `evaluate` (default CS guide) and `full` (complete authoring llms.txt).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum GuideTopic {
59    Method,
60    Syntax,
61    Data,
62    Rules,
63    Units,
64    Veto,
65    Composition,
66    AntiPatterns,
67    Evaluate,
68    Full,
69}
70
71impl GuideTopic {
72    pub const ALL: &[GuideTopic] = &[
73        GuideTopic::Method,
74        GuideTopic::Syntax,
75        GuideTopic::Data,
76        GuideTopic::Rules,
77        GuideTopic::Units,
78        GuideTopic::Veto,
79        GuideTopic::Composition,
80        GuideTopic::AntiPatterns,
81        GuideTopic::Evaluate,
82        GuideTopic::Full,
83    ];
84
85    pub const VALID_LIST: &str =
86        "method, syntax, data, rules, units, veto, composition, anti_patterns, evaluate, full";
87
88    pub fn as_str(self) -> &'static str {
89        match self {
90            GuideTopic::Method => "method",
91            GuideTopic::Syntax => "syntax",
92            GuideTopic::Data => "data",
93            GuideTopic::Rules => "rules",
94            GuideTopic::Units => "units",
95            GuideTopic::Veto => "veto",
96            GuideTopic::Composition => "composition",
97            GuideTopic::AntiPatterns => "anti_patterns",
98            GuideTopic::Evaluate => "evaluate",
99            GuideTopic::Full => "full",
100        }
101    }
102
103    pub fn parse(name: &str) -> Option<Self> {
104        Self::ALL.iter().copied().find(|t| t.as_str() == name)
105    }
106
107    /// Guide topic content from corresponding fragment.
108    pub fn section_text(self) -> &'static str {
109        match self {
110            GuideTopic::Method => METHOD,
111            GuideTopic::Syntax => SYNTAX,
112            GuideTopic::Data => DATA,
113            GuideTopic::Rules => RULES,
114            GuideTopic::Units => UNITS,
115            GuideTopic::Veto => VETO,
116            GuideTopic::Composition => COMPOSITION,
117            GuideTopic::AntiPatterns => ANTI_PATTERNS,
118            GuideTopic::Evaluate => EVALUATE_GUIDE,
119            GuideTopic::Full => LLMS_TXT,
120        }
121    }
122}
123
124/// Example source: path after `examples/` → body.
125pub struct ExampleResource {
126    pub path: &'static str,
127    pub body: &'static str,
128}
129
130pub const EXAMPLE_RESOURCES: &[ExampleResource] = &[
131    ExampleResource {
132        path: "01_coffee_order.lemma",
133        body: EXAMPLE_01_COFFEE_ORDER,
134    },
135    ExampleResource {
136        path: "02_library_fees.lemma",
137        body: EXAMPLE_02_LIBRARY_FEES,
138    },
139    ExampleResource {
140        path: "03_recipe_scaling.lemma",
141        body: EXAMPLE_03_RECIPE_SCALING,
142    },
143    ExampleResource {
144        path: "04_membership_benefits.lemma",
145        body: EXAMPLE_04_MEMBERSHIP_BENEFITS,
146    },
147    ExampleResource {
148        path: "05_weather_clothing.lemma",
149        body: EXAMPLE_05_WEATHER_CLOTHING,
150    },
151    ExampleResource {
152        path: "nl/tax/net_salary.lemma",
153        body: EXAMPLE_NL_TAX_NET_SALARY,
154    },
155];
156
157pub fn example_by_path(path: &str) -> Option<&'static str> {
158    EXAMPLE_RESOURCES
159        .iter()
160        .find(|e| e.path == path)
161        .map(|e| e.body)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::fs;
168    use std::path::PathBuf;
169
170    fn guide_dir() -> PathBuf {
171        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("documentation/guide")
172    }
173
174    fn concat_guide_fragments() -> String {
175        let mut entries = fs::read_dir(guide_dir())
176            .expect("BUG: documentation/guide/ must exist")
177            .filter_map(|entry| {
178                let entry = entry.ok()?;
179                let path = entry.path();
180                if path.extension()?.to_str()? == "md" {
181                    Some(path)
182                } else {
183                    None
184                }
185            })
186            .collect::<Vec<_>>();
187        entries.sort();
188        let mut content = String::new();
189        for (i, path) in entries.iter().enumerate() {
190            if i > 0 {
191                content.push_str("\n\n---\n\n");
192            }
193            content.push_str(
194                &fs::read_to_string(path)
195                    .unwrap_or_else(|e| panic!("BUG: read {}: {e}", path.display())),
196            );
197        }
198        content
199    }
200
201    #[test]
202    fn guide_topic_parse_round_trip() {
203        for topic in GuideTopic::ALL {
204            assert_eq!(GuideTopic::parse(topic.as_str()), Some(*topic));
205        }
206        assert!(GuideTopic::parse("temporal").is_none());
207        assert!(GuideTopic::parse("").is_none());
208    }
209
210    #[test]
211    fn full_guide_matches_concatenated_fragments() {
212        assert_eq!(GuideTopic::Full.section_text(), concat_guide_fragments());
213    }
214
215    #[test]
216    fn full_guide_does_not_embed_evaluate_guide() {
217        assert!(!GuideTopic::Full
218            .section_text()
219            .contains("**Evaluating loaded specs**"));
220    }
221}