Skip to main content

ucp_api/
lib.rs

1//! # UCP API
2//!
3//! High-level API for the Unified Content Protocol.
4//!
5//! This crate provides a convenient interface for working with UCP documents,
6//! combining the core types, engine operations, and UCL parsing into a
7//! unified API.
8//!
9//! ## Key Types
10//!
11//! - [`UcpClient`] - Main entry point for document manipulation
12//! - [`CodeGraphBuildResult`] - Code analysis for repositories
13//!
14//! ## Example
15//!
16//! ```ignore
17//! use ucp_api::UcpClient;
18//!
19//! let client = UcpClient::new();
20//! let mut doc = client.create_document();
21//!
22//! // Execute UCL commands
23//! let results = client.execute_ucl(&mut doc, "APPEND root text :: \"Hello!\"").unwrap();
24//! ```
25
26use 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
63/// UCP client for document manipulation
64pub struct UcpClient {
65    engine: Engine,
66}
67
68impl UcpClient {
69    /// Create a new UCP client with a fresh engine instance
70    pub fn new() -> Self {
71        Self {
72            engine: Engine::new(),
73        }
74    }
75
76    /// Create a new document
77    pub fn create_document(&self) -> Document {
78        Document::create()
79    }
80
81    /// Execute UCL commands on a document
82    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    /// Parse a full UCL document
91    pub fn parse_ucl(&self, ucl: &str) -> Result<UclDocument> {
92        parse(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))
93    }
94
95    /// Add a text block
96    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    /// Add a code block  
108    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    /// Get document as JSON
120    pub fn to_json(&self, doc: &Document) -> Result<String> {
121        // Serialize blocks
122        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                _ => {} // Other commands
264            }
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}