1use crate::PiConvo;
9use crate::provider::session_to_view;
10use crate::reader::PiSession;
11use toolpath::v1::{Document, Graph, GraphIdentity, GraphMeta, Path, PathOrRef};
12use toolpath_convo::DeriveConfig;
13
14pub fn derive_path(session: &PiSession, config: &DeriveConfig) -> Path {
19 toolpath_convo::derive_path(&session_to_view(session), config)
20}
21
22pub fn derive_graph(sessions: &[PiSession], title: Option<&str>, config: &DeriveConfig) -> Graph {
28 let id_suffix = sessions
29 .first()
30 .map(|s| s.header.id.chars().take(8).collect::<String>())
31 .unwrap_or_else(|| "empty".to_string());
32 let graph_id = format!("graph-pi-{}", id_suffix);
33
34 let paths: Vec<PathOrRef> = sessions
35 .iter()
36 .map(|s| PathOrRef::Path(Box::new(derive_path(s, config))))
37 .collect();
38
39 let meta = title.map(|t| GraphMeta {
40 title: Some(t.to_string()),
41 ..Default::default()
42 });
43
44 Graph {
45 graph: GraphIdentity { id: graph_id },
46 paths,
47 meta,
48 }
49}
50
51pub fn derive_project(
53 manager: &PiConvo,
54 project: &str,
55 title: Option<&str>,
56 config: &DeriveConfig,
57) -> crate::Result<Document> {
58 let sessions = manager.read_all_sessions(project)?;
59 let graph = derive_graph(&sessions, title, config);
60 Ok(Document::Graph(graph))
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::types::{AgentMessage, Entry, EntryBase, MessageContent, SessionHeader};
67 use std::collections::HashMap;
68 use std::path::PathBuf;
69
70 fn make_session(id: &str) -> PiSession {
71 let header = SessionHeader {
72 version: 3,
73 id: id.into(),
74 timestamp: "2026-04-16T00:00:00Z".into(),
75 cwd: "/tmp/p".into(),
76 parent_session: None,
77 extra: HashMap::new(),
78 };
79 let msg = Entry::Message {
80 base: EntryBase {
81 id: "u1".into(),
82 parent_id: None,
83 timestamp: "2026-04-16T00:00:01Z".into(),
84 },
85 message: AgentMessage::User {
86 content: MessageContent::Text("hi".into()),
87 timestamp: 1,
88 extra: HashMap::new(),
89 },
90 extra: HashMap::new(),
91 };
92 PiSession {
93 header: header.clone(),
94 entries: vec![Entry::Session(header), msg],
95 file_path: PathBuf::from("/tmp/fake.jsonl"),
96 parent: None,
97 }
98 }
99
100 #[test]
101 fn test_derive_path_wraps_provider() {
102 let session = make_session("abcd1234xxxx");
103 let path = derive_path(&session, &DeriveConfig::default());
104 assert_eq!(path.steps.len(), 1);
105 assert!(
106 path.path.id.starts_with("path-pi-"),
107 "got: {}",
108 path.path.id
109 );
110 }
111
112 #[test]
113 fn test_derive_path_respects_config_overrides() {
114 let session = make_session("abcd1234");
115 let cfg = DeriveConfig {
116 path_id: Some("custom-id".into()),
117 ..Default::default()
118 };
119 let path = derive_path(&session, &cfg);
120 assert_eq!(path.path.id, "custom-id");
121 }
122
123 #[test]
124 fn test_derive_graph_empty_sessions() {
125 let g = derive_graph(&[], None, &DeriveConfig::default());
126 assert!(g.paths.is_empty());
127 assert_eq!(g.graph.id, "graph-pi-empty");
128 }
129
130 #[test]
131 fn test_derive_graph_single_session() {
132 let s = make_session("sess-alpha");
133 let g = derive_graph(std::slice::from_ref(&s), None, &DeriveConfig::default());
134 assert_eq!(g.paths.len(), 1);
135 assert!(matches!(&g.paths[0], PathOrRef::Path(_)));
136 }
137
138 #[test]
139 fn test_derive_graph_multiple_sessions() {
140 let s1 = make_session("sess-one");
141 let s2 = make_session("sess-two");
142 let g = derive_graph(&[s1, s2], None, &DeriveConfig::default());
143 assert_eq!(g.paths.len(), 2);
144 }
145
146 #[test]
147 fn test_derive_graph_with_title() {
148 let s = make_session("sess-alpha");
149 let g = derive_graph(&[s], Some("My Release"), &DeriveConfig::default());
150 assert_eq!(
151 g.meta.as_ref().and_then(|m| m.title.as_deref()),
152 Some("My Release")
153 );
154 }
155
156 #[test]
157 fn test_derive_graph_no_title() {
158 let s = make_session("sess-alpha");
159 let g = derive_graph(&[s], None, &DeriveConfig::default());
160 assert!(g.meta.is_none());
161 }
162}