1use std::fmt::Write as _;
7
8use rto_graph::Explanation;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct VaultNote {
13 pub filename: String,
15 pub content: String,
17}
18
19#[must_use]
23pub fn note_name(key: &str) -> String {
24 let mut out = String::with_capacity(key.len());
25 let mut prev_dash = false;
26 for c in key.chars() {
27 if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
28 out.push(c);
29 prev_dash = false;
30 } else if !prev_dash {
31 out.push('-');
32 prev_dash = true;
33 }
34 }
35 out.trim_matches('-').to_owned()
36}
37
38#[must_use]
41pub fn render_note(ex: &Explanation) -> VaultNote {
42 let mut c = String::new();
43 c.push_str("---\n");
44 let _ = writeln!(c, "key: \"{}\"", ex.node.key.replace('"', "'"));
45 let _ = writeln!(c, "kind: {}", ex.node.kind);
46 if let Some(path) = &ex.node.path {
47 let _ = writeln!(c, "path: \"{path}\"");
48 }
49 c.push_str("---\n\n");
50 let _ = writeln!(c, "# {}", ex.node.name);
51
52 if !ex.outgoing.is_empty() {
53 c.push_str("\n## Outgoing\n\n");
54 for e in &ex.outgoing {
55 let _ = writeln!(
56 c,
57 "- {} ({}) → [[{}]]",
58 e.kind,
59 e.provenance,
60 note_name(&e.node)
61 );
62 }
63 }
64 if !ex.incoming.is_empty() {
65 c.push_str("\n## Incoming\n\n");
66 for e in &ex.incoming {
67 let _ = writeln!(
68 c,
69 "- [[{}]] {} ({}) →",
70 note_name(&e.node),
71 e.kind,
72 e.provenance
73 );
74 }
75 }
76
77 VaultNote {
78 filename: format!("{}.md", note_name(&ex.node.key)),
79 content: c,
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::{note_name, render_note};
86 use rto_graph::{EdgeRef, Explanation, NodeSummary};
87
88 #[test]
89 fn note_name_is_safe_and_stable() {
90 assert_eq!(
91 note_name("sym:rust:src/a.rs#Store"),
92 "sym-rust-src-a.rs-Store"
93 );
94 assert_eq!(note_name("adr:0001"), "adr-0001");
95 assert_eq!(note_name("file:src/main.rs"), "file-src-main.rs");
96 }
97
98 #[test]
99 fn render_note_emits_frontmatter_and_wikilinks() {
100 let ex = Explanation {
101 schema: rto_graph::SCHEMA,
102 node: NodeSummary {
103 key: "sym:rust:a.rs#main".into(),
104 kind: "fn".into(),
105 name: "main".into(),
106 path: Some("a.rs".into()),
107 lang: Some("rust".into()),
108 },
109 meta: serde_json::Value::Null,
110 outgoing: vec![EdgeRef {
111 kind: "calls".into(),
112 provenance: "derived",
113 confidence: None,
114 node: "sym:rust:a.rs#helper".into(),
115 }],
116 incoming: vec![EdgeRef {
117 kind: "references".into(),
118 provenance: "authored",
119 confidence: None,
120 node: "adr:0001".into(),
121 }],
122 };
123 let note = render_note(&ex);
124 assert_eq!(note.filename, "sym-rust-a.rs-main.md");
125 assert!(note.content.contains("kind: fn"));
126 assert!(note.content.contains("# main"));
127 assert!(
128 note.content
129 .contains("- calls (derived) → [[sym-rust-a.rs-helper]]")
130 );
131 assert!(
132 note.content
133 .contains("- [[adr-0001]] references (authored) →")
134 );
135 }
136}