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, 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
68pub struct UcpClient {
70 engine: Engine,
71}
72
73impl UcpClient {
74 pub fn new() -> Self {
76 Self {
77 engine: Engine::new(),
78 }
79 }
80
81 pub fn create_document(&self) -> Document {
83 Document::create()
84 }
85
86 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 pub fn parse_ucl(&self, ucl: &str) -> Result<UclDocument> {
97 parse(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))
98 }
99
100 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 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 pub fn to_json(&self, doc: &Document) -> Result<String> {
126 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 _ => {} }
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}