1use std::path::Path;
2
3use crate::{error::DkpResult, pack::loader::Pack};
4
5pub fn export_okf(pack: &Pack, output_dir: &Path) -> DkpResult<ExportStats> {
10 let mut stats = ExportStats::default();
11
12 if let Some(gf) = pack.load_glossary()? {
14 let dir = output_dir.join("terms");
15 std::fs::create_dir_all(&dir)?;
16 for t in &gf.terms {
17 let mut fm = String::from("---\n");
18 fm.push_str(&format!("id: {}\n", t.id));
19 fm.push_str("type: term\n");
20 fm.push_str(&format!("term: \"{}\"\n", escape_yaml(&t.term)));
21 if !t.aliases.is_empty() {
22 fm.push_str(&format!(
23 "aliases: [{}]\n",
24 t.aliases
25 .iter()
26 .map(|a| format!("\"{}\"", escape_yaml(a)))
27 .collect::<Vec<_>>()
28 .join(", ")
29 ));
30 }
31 if !t.tags.is_empty() {
32 fm.push_str(&format!(
33 "tags: [{}]\n",
34 t.tags
35 .iter()
36 .map(|t| format!("\"{}\"", escape_yaml(t)))
37 .collect::<Vec<_>>()
38 .join(", ")
39 ));
40 }
41 if let Some(s) = &t.stability {
42 fm.push_str(&format!("stability: {s}\n"));
43 }
44 if let Some(sr) = &t.source_ref {
45 fm.push_str(&format!("source_ref: {sr}\n"));
46 }
47 fm.push_str("---\n\n");
48 fm.push_str(&format!("# {}\n\n", t.term));
49 fm.push_str(&t.definition);
50 fm.push('\n');
51
52 std::fs::write(dir.join(format!("{}.md", t.id)), &fm)?;
53 stats.terms_written += 1;
54 }
55 }
56
57 if let Some(rf) = pack.load_rules()? {
59 let dir = output_dir.join("rules");
60 std::fs::create_dir_all(&dir)?;
61 for r in &rf.rules {
62 let polarity = format!("{:?}", r.polarity).to_lowercase();
63 let mut fm = String::from("---\n");
64 fm.push_str(&format!("id: {}\n", r.id));
65 fm.push_str("type: rule\n");
66 fm.push_str(&format!("title: \"{}\"\n", escape_yaml(&r.title)));
67 fm.push_str(&format!("polarity: {polarity}\n"));
68 if let Some(c) = r.confidence {
69 fm.push_str(&format!("confidence: {c}\n"));
70 }
71 if let Some(s) = &r.stability {
72 fm.push_str(&format!("stability: {s}\n"));
73 }
74 if let Some(sr) = &r.source_ref {
75 fm.push_str(&format!("source_ref: {sr}\n"));
76 }
77 if !r.tags.is_empty() {
78 fm.push_str(&format!(
79 "tags: [{}]\n",
80 r.tags
81 .iter()
82 .map(|t| format!("\"{}\"", escape_yaml(t)))
83 .collect::<Vec<_>>()
84 .join(", ")
85 ));
86 }
87 fm.push_str("---\n\n");
88 fm.push_str(&format!("# {}\n\n", r.title));
89 fm.push_str(&r.description);
90 fm.push('\n');
91
92 std::fs::write(dir.join(format!("{}.md", r.id)), &fm)?;
93 stats.rules_written += 1;
94 }
95 }
96
97 if let Some(cf) = pack.load_constraints()? {
99 let dir = output_dir.join("constraints");
100 std::fs::create_dir_all(&dir)?;
101 for c in cf.all_constraints() {
102 let mut fm = String::from("---\n");
103 fm.push_str(&format!("id: {}\n", c.id));
104 fm.push_str("type: constraint\n");
105 fm.push_str(&format!("title: \"{}\"\n", escape_yaml(&c.title)));
106 if let Some(s) = &c.stability {
107 fm.push_str(&format!("stability: {s}\n"));
108 }
109 if let Some(sr) = &c.source_ref {
110 fm.push_str(&format!("source_ref: {sr}\n"));
111 }
112 if !c.tags.is_empty() {
113 fm.push_str(&format!(
114 "tags: [{}]\n",
115 c.tags
116 .iter()
117 .map(|t| format!("\"{}\"", escape_yaml(t)))
118 .collect::<Vec<_>>()
119 .join(", ")
120 ));
121 }
122 fm.push_str("---\n\n");
123 fm.push_str(&format!("# {}\n\n", c.title));
124 fm.push_str(&c.description);
125 fm.push('\n');
126
127 std::fs::write(dir.join(format!("{}.md", c.id)), &fm)?;
128 stats.constraints_written += 1;
129 }
130 }
131
132 {
134 let chunks = pack.load_chunks()?;
135 if !chunks.is_empty() {
136 let dir = output_dir.join("chunks");
137 std::fs::create_dir_all(&dir)?;
138 for c in &chunks {
139 let mut fm = String::from("---\n");
140 fm.push_str(&format!("id: {}\n", c.id));
141 fm.push_str("type: chunk\n");
142 fm.push_str(&format!("title: \"{}\"\n", escape_yaml(&c.title)));
143 fm.push_str(&format!("source_ref: {}\n", c.source_ref));
144 if let Some(conf) = c.confidence {
145 fm.push_str(&format!("confidence: {conf}\n"));
146 }
147 if !c.tags.is_empty() {
148 fm.push_str(&format!(
149 "tags: [{}]\n",
150 c.tags
151 .iter()
152 .map(|t| format!("\"{}\"", escape_yaml(t)))
153 .collect::<Vec<_>>()
154 .join(", ")
155 ));
156 }
157 fm.push_str("---\n\n");
158 fm.push_str(&format!("# {}\n\n", c.title));
159 fm.push_str(&c.chunk_text);
160 fm.push('\n');
161
162 std::fs::write(dir.join(format!("{}.md", c.id)), &fm)?;
163 stats.chunks_written += 1;
164 }
165 }
166 }
167
168 if let Some(of) = pack.load_ontology()? {
170 let dir = output_dir.join("ontology");
171 std::fs::create_dir_all(&dir)?;
172 for e in &of.entity_types {
173 let mut fm = String::from("---\n");
174 fm.push_str(&format!("id: {}\n", e.id));
175 fm.push_str("type: entity\n");
176 fm.push_str(&format!("name: \"{}\"\n", escape_yaml(&e.name)));
177 if let Some(st) = &e.schema_org_type {
178 fm.push_str(&format!("schema_org_type: {st}\n"));
179 }
180 if !e.attributes.is_empty() {
181 fm.push_str(&format!(
182 "attributes: [{}]\n",
183 e.attributes
184 .iter()
185 .map(|a| format!("\"{}\"", escape_yaml(a)))
186 .collect::<Vec<_>>()
187 .join(", ")
188 ));
189 }
190 fm.push_str("---\n\n");
191 fm.push_str(&format!("# {}\n\n", e.name));
192 fm.push_str(&e.description);
193 fm.push('\n');
194
195 std::fs::write(dir.join(format!("{}.md", e.id)), &fm)?;
196 stats.ontology_written += 1;
197 }
198 }
199
200 Ok(stats)
201}
202
203fn escape_yaml(s: &str) -> String {
204 s.replace('"', "\\\"")
205}
206
207#[derive(Debug, Default)]
208pub struct ExportStats {
209 pub terms_written: usize,
210 pub rules_written: usize,
211 pub constraints_written: usize,
212 pub chunks_written: usize,
213 pub ontology_written: usize,
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::okf::parser::parse_okf_dir;
220 use tempfile::TempDir;
221
222 fn open_pack_with_glossary(tmp: &TempDir) -> Pack {
223 std::fs::write(
224 tmp.path().join("manifest.json"),
225 r#"{
226 "spec": "1.0.0",
227 "name": "test-pack",
228 "version": "1.0.0",
229 "domain": "testing",
230 "audience": "internal",
231 "intended_use": "unit tests",
232 "known_limitations": "none",
233 "update_date": "2026-01-01"
234 }"#,
235 )
236 .unwrap();
237 let pack = Pack::open(tmp.path()).unwrap();
238 std::fs::create_dir_all(pack.machine_dir()).unwrap();
239 std::fs::write(
240 pack.machine_file("glossary.json"),
241 r#"{"terms": [{"id": "t1", "term": "Widget \"Pro\"", "definition": "A useful thing.", "tags": ["core"]}]}"#,
242 )
243 .unwrap();
244 pack
245 }
246
247 #[test]
248 fn export_then_parse_round_trip_preserves_content() {
249 let tmp = TempDir::new().unwrap();
250 let pack = open_pack_with_glossary(&tmp);
251 let out_dir = tmp.path().join("okf-out");
252
253 let stats = export_okf(&pack, &out_dir).unwrap();
254 assert_eq!(stats.terms_written, 1);
255
256 let concepts = parse_okf_dir(&out_dir).unwrap();
257 assert_eq!(concepts.len(), 1);
258 let concept = &concepts[0];
259 assert_eq!(
260 concept.frontmatter.get("type").and_then(|v| v.as_str()),
261 Some("term")
262 );
263 assert_eq!(
264 concept.frontmatter.get("id").and_then(|v| v.as_str()),
265 Some("t1")
266 );
267 assert!(concept.body.contains("A useful thing."));
268 }
269
270 #[test]
271 fn export_okf_empty_pack_writes_nothing() {
272 let tmp = TempDir::new().unwrap();
273 std::fs::write(
274 tmp.path().join("manifest.json"),
275 r#"{
276 "spec": "1.0.0",
277 "name": "test-pack",
278 "version": "1.0.0",
279 "domain": "testing",
280 "audience": "internal",
281 "intended_use": "unit tests",
282 "known_limitations": "none",
283 "update_date": "2026-01-01"
284 }"#,
285 )
286 .unwrap();
287 let pack = Pack::open(tmp.path()).unwrap();
288 let out_dir = tmp.path().join("okf-out");
289
290 let stats = export_okf(&pack, &out_dir).unwrap();
291 assert_eq!(stats.terms_written, 0);
292 assert_eq!(stats.rules_written, 0);
293 assert_eq!(stats.chunks_written, 0);
294 }
295}