1use std::str::FromStr;
27
28use ucl_parser::{parse, parse_commands, UclDocument};
29pub use ucm_core::PortableDocument;
30use ucm_core::{Block, BlockId, Content, Document, EdgeType, Error, Result};
31use ucm_engine::{Engine, Operation, OperationResult};
32
33#[cfg(not(target_arch = "wasm32"))]
34pub use ucp_codegraph::{
35 approximate_prompt_tokens, build_code_graph, build_code_graph_incremental,
36 canonical_codegraph_json, canonical_fingerprint, codegraph_prompt_projection,
37 codegraph_prompt_projection_with_config, export_codegraph_context,
38 export_codegraph_context_with_config, is_codegraph_document, render_codegraph_context_prompt,
39 resolve_codegraph_selector, validate_code_graph_profile, CodeGraphBuildInput,
40 CodeGraphBuildResult, CodeGraphBuildStatus, CodeGraphCoderef, CodeGraphContextEdgeExport,
41 CodeGraphContextExport, CodeGraphContextFrontierAction, CodeGraphContextHeuristics,
42 CodeGraphContextNodeExport, CodeGraphContextSession, CodeGraphContextSummary,
43 CodeGraphContextUpdate, CodeGraphDetailLevel, CodeGraphDiagnostic, CodeGraphExpandMode,
44 CodeGraphExportConfig, CodeGraphExportMode, CodeGraphExtractorConfig, CodeGraphFindQuery,
45 CodeGraphHiddenLevelSummary, CodeGraphIncrementalBuildInput, CodeGraphIncrementalStats,
46 CodeGraphNavigator, CodeGraphNavigatorSession, CodeGraphNodeSummary, CodeGraphPathHop,
47 CodeGraphPathResult, CodeGraphPromptProjectionConfig, CodeGraphPrunePolicy,
48 CodeGraphRecommendedActionsResult, CodeGraphRenderConfig, CodeGraphSelectionExplanation,
49 CodeGraphSelectionOrigin, CodeGraphSelectionOriginKind, CodeGraphSessionDiff,
50 CodeGraphSeverity, CodeGraphStats, CodeGraphTraversalConfig, CodeGraphValidationResult,
51 HydratedSourceExcerpt, CODEGRAPH_EXTRACTOR_VERSION, CODEGRAPH_PROFILE_MARKER,
52 CODEGRAPH_PROFILE_VERSION,
53};
54#[cfg(not(target_arch = "wasm32"))]
55pub use ucp_graph::{
56 GraphDetailLevel, GraphExport, GraphExportEdge, GraphExportNode, GraphFindQuery,
57 GraphNavigator, GraphNeighborMode, GraphNodeRecord, GraphNodeSummary, GraphPathHop,
58 GraphPathResult, GraphSelectionExplanation, GraphSelectionOrigin, GraphSelectionOriginKind,
59 GraphSession, GraphSessionDiff, GraphSessionNode, GraphSessionSummary, GraphSessionUpdate,
60 GraphStoreObservability, GraphStoreStats, InMemoryGraphStore, SqliteGraphStore,
61};
62
63pub struct UcpClient {
65 engine: Engine,
66}
67
68impl UcpClient {
69 pub fn new() -> Self {
71 Self {
72 engine: Engine::new(),
73 }
74 }
75
76 pub fn create_document(&self) -> Document {
78 Document::create()
79 }
80
81 pub fn execute_ucl(&self, doc: &mut Document, ucl: &str) -> Result<Vec<OperationResult>> {
83 let commands =
84 parse_commands(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))?;
85
86 let ops = self.commands_to_operations(commands)?;
87 self.engine.execute_batch(doc, ops)
88 }
89
90 pub fn parse_ucl(&self, ucl: &str) -> Result<UclDocument> {
92 parse(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))
93 }
94
95 pub fn add_text(
97 &self,
98 doc: &mut Document,
99 parent: &BlockId,
100 text: &str,
101 role: Option<&str>,
102 ) -> Result<BlockId> {
103 let block = Block::new(Content::text(text), role);
104 doc.add_block(block, parent)
105 }
106
107 pub fn add_code(
109 &self,
110 doc: &mut Document,
111 parent: &BlockId,
112 lang: &str,
113 code: &str,
114 ) -> Result<BlockId> {
115 let block = Block::new(Content::code(lang, code), None);
116 doc.add_block(block, parent)
117 }
118
119 pub fn to_json(&self, doc: &Document) -> Result<String> {
121 let blocks: Vec<_> = doc.blocks.values().collect();
123 serde_json::to_string_pretty(&blocks)
124 .map_err(|e| Error::Internal(format!("Serialization error: {}", e)))
125 }
126
127 fn commands_to_operations(&self, commands: Vec<ucl_parser::Command>) -> Result<Vec<Operation>> {
128 let mut ops = Vec::new();
129 for cmd in commands {
130 match cmd {
131 ucl_parser::Command::Edit(e) => {
132 let block_id: BlockId = e
133 .block_id
134 .parse()
135 .map_err(|_| Error::InvalidBlockId(e.block_id.clone()))?;
136 ops.push(Operation::Edit {
137 block_id,
138 path: e.path.to_string(),
139 value: e.value.to_json(),
140 operator: match e.operator {
141 ucl_parser::Operator::Set => ucm_engine::EditOperator::Set,
142 ucl_parser::Operator::Append => ucm_engine::EditOperator::Append,
143 ucl_parser::Operator::Remove => ucm_engine::EditOperator::Remove,
144 ucl_parser::Operator::Increment => ucm_engine::EditOperator::Increment,
145 ucl_parser::Operator::Decrement => ucm_engine::EditOperator::Decrement,
146 },
147 });
148 }
149 ucl_parser::Command::Append(a) => {
150 let parent_id: BlockId = a
151 .parent_id
152 .parse()
153 .map_err(|_| Error::InvalidBlockId(a.parent_id.clone()))?;
154 let content = match a.content_type {
155 ucl_parser::ContentType::Text => Content::text(&a.content),
156 ucl_parser::ContentType::Code => Content::code("", &a.content),
157 _ => Content::text(&a.content),
158 };
159 ops.push(Operation::Append {
160 parent_id,
161 content,
162 label: a.properties.get("label").and_then(|v| match v {
163 ucl_parser::Value::String(s) => Some(s.clone()),
164 _ => None,
165 }),
166 tags: Vec::new(),
167 semantic_role: a.properties.get("role").and_then(|v| match v {
168 ucl_parser::Value::String(s) => Some(s.clone()),
169 _ => None,
170 }),
171 index: a.index,
172 });
173 }
174 ucl_parser::Command::Delete(d) => {
175 if let Some(id) = d.block_id {
176 let block_id: BlockId =
177 id.parse().map_err(|_| Error::InvalidBlockId(id.clone()))?;
178 ops.push(Operation::Delete {
179 block_id,
180 cascade: d.cascade,
181 preserve_children: d.preserve_children,
182 });
183 }
184 }
185 ucl_parser::Command::Move(m) => {
186 let block_id: BlockId = m
187 .block_id
188 .parse()
189 .map_err(|_| Error::InvalidBlockId(m.block_id.clone()))?;
190 match m.target {
191 ucl_parser::MoveTarget::ToParent { parent_id, index } => {
192 let new_parent: BlockId = parent_id
193 .parse()
194 .map_err(|_| Error::InvalidBlockId(parent_id.clone()))?;
195 ops.push(Operation::MoveToTarget {
196 block_id,
197 target: ucm_engine::MoveTarget::ToParent {
198 parent_id: new_parent,
199 index,
200 },
201 });
202 }
203 ucl_parser::MoveTarget::Before { sibling_id } => {
204 let sibling: BlockId = sibling_id
205 .parse()
206 .map_err(|_| Error::InvalidBlockId(sibling_id.clone()))?;
207 ops.push(Operation::MoveToTarget {
208 block_id,
209 target: ucm_engine::MoveTarget::Before {
210 sibling_id: sibling,
211 },
212 });
213 }
214 ucl_parser::MoveTarget::After { sibling_id } => {
215 let sibling: BlockId = sibling_id
216 .parse()
217 .map_err(|_| Error::InvalidBlockId(sibling_id.clone()))?;
218 ops.push(Operation::MoveToTarget {
219 block_id,
220 target: ucm_engine::MoveTarget::After {
221 sibling_id: sibling,
222 },
223 });
224 }
225 }
226 }
227 ucl_parser::Command::Prune(p) => {
228 let condition = match p.target {
229 ucl_parser::PruneTarget::Unreachable => {
230 Some(ucm_engine::PruneCondition::Unreachable)
231 }
232 _ => None,
233 };
234 ops.push(Operation::Prune { condition });
235 }
236 ucl_parser::Command::Link(l) => {
237 let source: BlockId = l
238 .source_id
239 .parse()
240 .map_err(|_| Error::InvalidBlockId(l.source_id.clone()))?;
241 let target: BlockId = l
242 .target_id
243 .parse()
244 .map_err(|_| Error::InvalidBlockId(l.target_id.clone()))?;
245 let edge_type =
246 EdgeType::from_str(&l.edge_type).unwrap_or(EdgeType::References);
247 ops.push(Operation::Link {
248 source,
249 edge_type,
250 target,
251 metadata: None,
252 });
253 }
254 ucl_parser::Command::Snapshot(s) => match s {
255 ucl_parser::SnapshotCommand::Create { name, description } => {
256 ops.push(Operation::CreateSnapshot { name, description });
257 }
258 ucl_parser::SnapshotCommand::Restore { name } => {
259 ops.push(Operation::RestoreSnapshot { name });
260 }
261 _ => {}
262 },
263 _ => {} }
265 }
266 Ok(ops)
267 }
268}
269
270impl Default for UcpClient {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_create_document() {
282 let client = UcpClient::new();
283 let doc = client.create_document();
284 assert_eq!(doc.block_count(), 1);
285 }
286
287 #[test]
288 fn test_add_text() {
289 let client = UcpClient::new();
290 let mut doc = client.create_document();
291 let root = doc.root;
292
293 let id = client
294 .add_text(&mut doc, &root, "Hello, world!", Some("intro"))
295 .unwrap();
296 assert_eq!(doc.block_count(), 2);
297 assert!(doc.get_block(&id).is_some());
298 }
299}