convergio_knowledge/
seed_parsers.rs1use std::path::Path;
8
9use tracing::warn;
10
11pub type Entry = (String, String); pub fn parse_learnings(content: &str) -> Vec<Entry> {
15 let mut out = Vec::new();
16 let mut active = false;
17 for line in content.lines() {
18 if line.contains("Key learnings") && line.starts_with('#') {
19 active = true;
20 continue;
21 }
22 if active && line.starts_with('#') {
23 break;
24 }
25 if active {
26 if let Some(t) = line.trim().strip_prefix("- ") {
27 if !t.is_empty() {
28 out.push((t.replace("**", ""), "learning".into()));
29 }
30 }
31 }
32 }
33 out
34}
35
36pub fn parse_adr_files(root: &Path) -> Vec<Entry> {
38 let Ok(dir) = std::fs::read_dir(root.join("docs/adr")) else {
39 warn!("seed: docs/adr not found, skipping ADRs");
40 return vec![];
41 };
42 let mut out = Vec::new();
43 for entry in dir.flatten() {
44 let fname = entry.file_name().to_string_lossy().to_string();
45 if !fname.starts_with("ADR-") || !fname.ends_with(".md") {
46 continue;
47 }
48 let Ok(raw) = std::fs::read_to_string(entry.path()) else {
49 continue;
50 };
51 let title = raw
52 .lines()
53 .find(|l| l.starts_with("# "))
54 .map(|l| l.trim_start_matches("# ").to_string())
55 .unwrap_or_else(|| fname.clone());
56 let mut secs = String::new();
57 let mut cap = false;
58 for line in raw.lines() {
59 if line.starts_with("## Decision") || line.starts_with("## Consequences") {
60 cap = true;
61 } else if cap && line.starts_with("## ") {
62 cap = false;
63 }
64 if cap {
65 secs.push_str(line);
66 secs.push('\n');
67 }
68 }
69 let text = if secs.is_empty() {
70 format!("{title}\n\n{}", &raw[..raw.len().min(500)])
71 } else {
72 format!("{title}\n\n{secs}")
73 };
74 out.push((text, "decision".into()));
75 }
76 out
77}
78
79pub fn parse_phases(content: &str) -> Vec<Entry> {
81 let mut out = Vec::new();
82 let mut heading: Option<String> = None;
83 let mut buf = String::new();
84 let is_section = |line: &str| {
85 line.starts_with("## ") && line.len() > 4 && line.as_bytes()[3].is_ascii_digit()
86 };
87 for line in content.lines() {
88 if is_section(line) || (line.starts_with("## ") && heading.is_some()) {
89 if let Some(h) = heading.take() {
90 if !buf.trim().is_empty() {
91 out.push((format!("{h}\n{}", buf.trim()), "learning".into()));
92 }
93 }
94 buf.clear();
95 if is_section(line) {
96 heading = Some(line.to_string());
97 }
98 continue;
99 }
100 if heading.is_some() {
101 buf.push_str(line);
102 buf.push('\n');
103 }
104 }
105 if let Some(h) = heading {
106 if !buf.trim().is_empty() {
107 out.push((format!("{h}\n{}", buf.trim()), "learning".into()));
108 }
109 }
110 out
111}
112
113pub fn parse_constitution(content: &str) -> Vec<Entry> {
115 let mut out = Vec::new();
116 let mut rule = String::new();
117 for line in content.lines() {
118 let t = line.trim();
119 let numbered = t.len() > 2 && t.as_bytes()[0].is_ascii_digit() && t.contains(". ");
120 if numbered {
121 if !rule.is_empty() {
122 out.push((rule.clone(), "decision".into()));
123 }
124 rule = t.to_string();
125 } else if !rule.is_empty() && !t.is_empty() && !t.starts_with('#') {
126 rule.push(' ');
127 rule.push_str(t);
128 } else if t.starts_with('#') && !rule.is_empty() {
129 out.push((rule.clone(), "decision".into()));
130 rule.clear();
131 }
132 }
133 if !rule.is_empty() {
134 out.push((rule, "decision".into()));
135 }
136 out
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn parse_learnings_extracts_and_strips_bold() {
145 let md = "### Key learnings\n- First (#1)\n- **Bold (#2)**\n## Next";
146 let items = parse_learnings(md);
147 assert_eq!(items.len(), 2);
148 assert_eq!(items[0].1, "learning");
149 assert!(!items[1].0.contains("**"));
150 }
151
152 #[test]
153 fn parse_phases_extracts() {
154 let md = "# Top\n## 1. First\nOne\n## 2. Second\nTwo\n## Other\n";
155 let items = parse_phases(md);
156 assert_eq!(items.len(), 2);
157 assert!(items[0].0.contains("1. First"));
158 }
159
160 #[test]
161 fn parse_constitution_numbered() {
162 let md = "# Title\n1. First rule\n2. Second rule\nwith more\n";
163 let items = parse_constitution(md);
164 assert_eq!(items.len(), 2);
165 assert!(items[1].0.contains("with more"));
166 }
167}