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