Skip to main content

ucl_parser/
ast.rs

1//! Abstract Syntax Tree for UCL documents.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// A complete UCL document
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct UclDocument {
9    /// Structure declarations (parent -> children)
10    pub structure: HashMap<String, Vec<String>>,
11    /// Block definitions
12    pub blocks: Vec<BlockDef>,
13    /// Commands to execute
14    pub commands: Vec<Command>,
15}
16
17impl UclDocument {
18    pub fn new() -> Self {
19        Self {
20            structure: HashMap::new(),
21            blocks: Vec::new(),
22            commands: Vec::new(),
23        }
24    }
25}
26
27impl Default for UclDocument {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33/// Block definition
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct BlockDef {
36    /// Content type (text, table, code, etc.)
37    pub content_type: ContentType,
38    /// Block ID
39    pub id: String,
40    /// Properties (label, tags, etc.)
41    pub properties: HashMap<String, Value>,
42    /// Content literal
43    pub content: String,
44}
45
46/// Content type
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum ContentType {
50    Text,
51    Table,
52    Code,
53    Math,
54    Media,
55    Json,
56    Binary,
57    Composite,
58}
59
60impl ContentType {
61    pub fn parse_content_type(s: &str) -> Option<Self> {
62        match s.to_lowercase().as_str() {
63            "text" => Some(Self::Text),
64            "table" => Some(Self::Table),
65            "code" => Some(Self::Code),
66            "math" => Some(Self::Math),
67            "media" => Some(Self::Media),
68            "json" => Some(Self::Json),
69            "binary" => Some(Self::Binary),
70            "composite" => Some(Self::Composite),
71            _ => None,
72        }
73    }
74}
75
76/// UCL command
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub enum Command {
79    // Document modification commands
80    Edit(EditCommand),
81    Move(MoveCommand),
82    Append(AppendCommand),
83    Delete(DeleteCommand),
84    Prune(PruneCommand),
85    Fold(FoldCommand),
86    Link(LinkCommand),
87    Unlink(UnlinkCommand),
88    Snapshot(SnapshotCommand),
89    Transaction(TransactionCommand),
90    Atomic(Vec<Command>),
91    WriteSection(WriteSectionCommand),
92
93    // Agent traversal commands
94    Goto(GotoCommand),
95    Back(BackCommand),
96    Expand(ExpandCommand),
97    Follow(FollowCommand),
98    Path(PathFindCommand),
99    Search(SearchCommand),
100    Find(FindCommand),
101    View(ViewCommand),
102
103    // Context window commands
104    Context(ContextCommand),
105}
106
107/// EDIT command
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct EditCommand {
110    pub block_id: String,
111    pub path: Path,
112    pub operator: Operator,
113    pub value: Value,
114    pub condition: Option<Condition>,
115}
116
117/// MOVE command
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct MoveCommand {
120    pub block_id: String,
121    pub target: MoveTarget,
122}
123
124/// Move target
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub enum MoveTarget {
127    ToParent {
128        parent_id: String,
129        index: Option<usize>,
130    },
131    Before {
132        sibling_id: String,
133    },
134    After {
135        sibling_id: String,
136    },
137}
138
139/// APPEND command
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct AppendCommand {
142    pub parent_id: String,
143    pub content_type: ContentType,
144    pub properties: HashMap<String, Value>,
145    pub content: String,
146    pub index: Option<usize>,
147}
148
149/// DELETE command
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct DeleteCommand {
152    pub block_id: Option<String>,
153    pub cascade: bool,
154    pub preserve_children: bool,
155    pub condition: Option<Condition>,
156}
157
158/// PRUNE command
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct PruneCommand {
161    pub target: PruneTarget,
162    pub dry_run: bool,
163}
164
165/// Prune target
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub enum PruneTarget {
168    Unreachable,
169    Where(Condition),
170}
171
172/// FOLD command
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct FoldCommand {
175    pub block_id: String,
176    pub depth: Option<usize>,
177    pub max_tokens: Option<usize>,
178    pub preserve_tags: Vec<String>,
179}
180
181/// LINK command
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct LinkCommand {
184    pub source_id: String,
185    pub edge_type: String,
186    pub target_id: String,
187    pub metadata: HashMap<String, Value>,
188}
189
190/// UNLINK command
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct UnlinkCommand {
193    pub source_id: String,
194    pub edge_type: String,
195    pub target_id: String,
196}
197
198/// SNAPSHOT command
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub enum SnapshotCommand {
201    Create {
202        name: String,
203        description: Option<String>,
204    },
205    Restore {
206        name: String,
207    },
208    List,
209    Delete {
210        name: String,
211    },
212    Diff {
213        name1: String,
214        name2: String,
215    },
216}
217
218/// Transaction command
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub enum TransactionCommand {
221    Begin { name: Option<String> },
222    Commit { name: Option<String> },
223    Rollback { name: Option<String> },
224}
225
226/// WRITE_SECTION command - write markdown to a section
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct WriteSectionCommand {
229    /// Target section block ID
230    pub section_id: String,
231    /// Markdown content to write
232    pub markdown: String,
233    /// Base heading level for relative heading adjustment
234    pub base_heading_level: Option<usize>,
235}
236
237// ============================================================================
238// Agent Traversal Commands
239// ============================================================================
240
241/// GOTO command - navigate cursor to a specific block
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243pub struct GotoCommand {
244    /// Target block ID to navigate to
245    pub block_id: String,
246}
247
248/// BACK command - go back in navigation history
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct BackCommand {
251    /// Number of steps to go back (default: 1)
252    pub steps: usize,
253}
254
255impl Default for BackCommand {
256    fn default() -> Self {
257        Self { steps: 1 }
258    }
259}
260
261/// Direction for graph expansion
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "lowercase")]
264#[derive(Default)]
265pub enum ExpandDirection {
266    /// Expand to children (BFS)
267    #[default]
268    Down,
269    /// Expand to ancestors
270    Up,
271    /// Expand both directions
272    Both,
273    /// Follow semantic edges only
274    Semantic,
275}
276
277impl ExpandDirection {
278    pub fn parse(s: &str) -> Option<Self> {
279        match s.to_lowercase().as_str() {
280            "down" => Some(Self::Down),
281            "up" => Some(Self::Up),
282            "both" => Some(Self::Both),
283            "semantic" => Some(Self::Semantic),
284            _ => None,
285        }
286    }
287}
288
289/// View mode for block content display
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291#[serde(rename_all = "lowercase")]
292#[derive(Default)]
293pub enum ViewMode {
294    /// Show full content
295    #[default]
296    Full,
297    /// Show first N characters as preview
298    Preview { length: usize },
299    /// Show only metadata (role, tags, edge counts)
300    Metadata,
301    /// Show only block IDs and structure
302    IdsOnly,
303}
304
305impl ViewMode {
306    pub fn parse(s: &str) -> Option<Self> {
307        match s.to_lowercase().as_str() {
308            "full" => Some(Self::Full),
309            "preview" => Some(Self::Preview { length: 100 }),
310            "metadata" => Some(Self::Metadata),
311            "ids" | "idsonly" | "ids_only" => Some(Self::IdsOnly),
312            _ => None,
313        }
314    }
315}
316
317/// Filter criteria for traversal operations
318#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
319pub struct TraversalFilterCriteria {
320    /// Include only blocks with these roles
321    pub include_roles: Vec<String>,
322    /// Exclude blocks with these roles
323    pub exclude_roles: Vec<String>,
324    /// Include only blocks with these tags
325    pub include_tags: Vec<String>,
326    /// Exclude blocks with these tags
327    pub exclude_tags: Vec<String>,
328    /// Filter by content pattern (regex)
329    pub content_pattern: Option<String>,
330    /// Filter by edge types to follow
331    pub edge_types: Vec<String>,
332}
333
334/// EXPAND command - expand from a block in a direction
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct ExpandCommand {
337    /// Block ID to expand from
338    pub block_id: String,
339    /// Direction to expand
340    pub direction: ExpandDirection,
341    /// Maximum depth to expand
342    pub depth: usize,
343    /// View mode for results
344    pub mode: Option<ViewMode>,
345    /// Filter criteria
346    pub filter: Option<TraversalFilterCriteria>,
347}
348
349impl Default for ExpandCommand {
350    fn default() -> Self {
351        Self {
352            block_id: String::new(),
353            direction: ExpandDirection::Down,
354            depth: 1,
355            mode: None,
356            filter: None,
357        }
358    }
359}
360
361/// FOLLOW command - follow edges from a block
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363pub struct FollowCommand {
364    /// Source block ID
365    pub source_id: String,
366    /// Edge types to follow
367    pub edge_types: Vec<String>,
368    /// Optional specific target block
369    pub target_id: Option<String>,
370}
371
372/// PATH command - find path between two blocks
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374pub struct PathFindCommand {
375    /// Starting block ID
376    pub from_id: String,
377    /// Target block ID
378    pub to_id: String,
379    /// Maximum path length
380    pub max_length: Option<usize>,
381}
382
383/// SEARCH command - semantic search (requires RAG provider)
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct SearchCommand {
386    /// Search query string
387    pub query: String,
388    /// Maximum number of results
389    pub limit: Option<usize>,
390    /// Minimum similarity threshold (0.0-1.0)
391    pub min_similarity: Option<f32>,
392    /// Filter criteria for results
393    pub filter: Option<TraversalFilterCriteria>,
394}
395
396impl Default for SearchCommand {
397    fn default() -> Self {
398        Self {
399            query: String::new(),
400            limit: Some(10),
401            min_similarity: None,
402            filter: None,
403        }
404    }
405}
406
407/// FIND command - pattern-based search (no RAG needed)
408#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
409pub struct FindCommand {
410    /// Find by semantic role
411    pub role: Option<String>,
412    /// Find by tag
413    pub tag: Option<String>,
414    /// Find by label
415    pub label: Option<String>,
416    /// Find by content pattern (regex)
417    pub pattern: Option<String>,
418}
419
420/// Target for VIEW command
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422pub enum ViewTarget {
423    /// View a specific block
424    Block(String),
425    /// View current cursor neighborhood
426    Neighborhood,
427}
428
429/// VIEW command - view block or neighborhood content
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431pub struct ViewCommand {
432    /// What to view
433    pub target: ViewTarget,
434    /// View mode
435    pub mode: ViewMode,
436    /// Depth for neighborhood view
437    pub depth: Option<usize>,
438}
439
440impl Default for ViewCommand {
441    fn default() -> Self {
442        Self {
443            target: ViewTarget::Neighborhood,
444            mode: ViewMode::Full,
445            depth: None,
446        }
447    }
448}
449
450// ============================================================================
451// Context Window Commands
452// ============================================================================
453
454/// CTX command - context window operations
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456pub enum ContextCommand {
457    /// Add block(s) to context
458    Add(ContextAddCommand),
459    /// Remove block from context
460    Remove { block_id: String },
461    /// Clear entire context window
462    Clear,
463    /// Expand context in a direction
464    Expand(ContextExpandCommand),
465    /// Compress context using a method
466    Compress { method: CompressionMethod },
467    /// Prune context based on criteria
468    Prune(ContextPruneCommand),
469    /// Render context for LLM prompt
470    Render { format: Option<RenderFormat> },
471    /// Get context statistics
472    Stats,
473    /// Set/clear focus block
474    Focus { block_id: Option<String> },
475}
476
477/// Target for CTX ADD command
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub enum ContextAddTarget {
480    /// Add a single block
481    Block(String),
482    /// Add all results from last search/find
483    Results,
484    /// Add all children of a block
485    Children { parent_id: String },
486    /// Add all blocks in a path
487    Path { from_id: String, to_id: String },
488}
489
490/// CTX ADD command options
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492pub struct ContextAddCommand {
493    /// What to add
494    pub target: ContextAddTarget,
495    /// Reason for inclusion (for tracking)
496    pub reason: Option<String>,
497    /// Custom relevance score (0.0-1.0)
498    pub relevance: Option<f32>,
499}
500
501/// CTX EXPAND command options
502#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503pub struct ContextExpandCommand {
504    /// Direction to expand
505    pub direction: ExpandDirection,
506    /// Maximum depth
507    pub depth: Option<usize>,
508    /// Token budget for auto-expansion
509    pub token_budget: Option<usize>,
510}
511
512/// CTX PRUNE command criteria
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
514pub struct ContextPruneCommand {
515    /// Remove blocks below this relevance threshold
516    pub min_relevance: Option<f32>,
517    /// Remove blocks not accessed in this many seconds
518    pub max_age_secs: Option<u64>,
519}
520
521/// Compression method for context
522#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
523#[serde(rename_all = "lowercase")]
524pub enum CompressionMethod {
525    /// Truncate low-relevance content
526    Truncate,
527    /// Summarize content (requires summarizer)
528    Summarize,
529    /// Keep only structure, no content
530    StructureOnly,
531}
532
533impl CompressionMethod {
534    pub fn parse(s: &str) -> Option<Self> {
535        match s.to_lowercase().as_str() {
536            "truncate" => Some(Self::Truncate),
537            "summarize" => Some(Self::Summarize),
538            "structure_only" | "structureonly" | "structure" => Some(Self::StructureOnly),
539            _ => None,
540        }
541    }
542}
543
544/// Render format for context output
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
546#[serde(rename_all = "lowercase")]
547#[derive(Default)]
548pub enum RenderFormat {
549    /// Default format
550    #[default]
551    Default,
552    /// Use short IDs (1, 2, 3...) for token efficiency
553    ShortIds,
554    /// Render as markdown
555    Markdown,
556}
557
558impl RenderFormat {
559    pub fn parse(s: &str) -> Option<Self> {
560        match s.to_lowercase().as_str() {
561            "default" => Some(Self::Default),
562            "short_ids" | "shortids" | "short" => Some(Self::ShortIds),
563            "markdown" | "md" => Some(Self::Markdown),
564            _ => None,
565        }
566    }
567}
568
569/// Path expression
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
571pub struct Path {
572    pub segments: Vec<PathSegment>,
573}
574
575impl Path {
576    pub fn new(segments: Vec<PathSegment>) -> Self {
577        Self { segments }
578    }
579
580    pub fn simple(name: &str) -> Self {
581        Self {
582            segments: vec![PathSegment::Property(name.to_string())],
583        }
584    }
585}
586
587impl std::fmt::Display for Path {
588    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589        write!(
590            f,
591            "{}",
592            self.segments
593                .iter()
594                .map(|s| match s {
595                    PathSegment::Property(p) => p.clone(),
596                    PathSegment::Index(i) => format!("[{}]", i),
597                    PathSegment::Slice { start, end } => match (start, end) {
598                        (Some(s), Some(e)) => format!("[{}:{}]", s, e),
599                        (Some(s), None) => format!("[{}:]", s),
600                        (None, Some(e)) => format!("[:{}]", e),
601                        (None, None) => "[:]".to_string(),
602                    },
603                    PathSegment::JsonPath(p) => format!("${}", p),
604                })
605                .collect::<Vec<_>>()
606                .join(".")
607        )
608    }
609}
610
611/// Path segment
612#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
613pub enum PathSegment {
614    Property(String),
615    Index(i64),
616    Slice {
617        start: Option<i64>,
618        end: Option<i64>,
619    },
620    JsonPath(String),
621}
622
623/// Operator
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
625pub enum Operator {
626    Set,       // =
627    Append,    // +=
628    Remove,    // -=
629    Increment, // ++
630    Decrement, // --
631}
632
633/// Value literal
634#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
635#[serde(untagged)]
636pub enum Value {
637    Null,
638    Bool(bool),
639    Number(f64),
640    String(String),
641    Array(Vec<Value>),
642    Object(HashMap<String, Value>),
643    BlockRef(String),
644}
645
646impl Value {
647    pub fn to_json(&self) -> serde_json::Value {
648        match self {
649            Value::Null => serde_json::Value::Null,
650            Value::Bool(b) => serde_json::Value::Bool(*b),
651            Value::Number(n) => serde_json::json!(*n),
652            Value::String(s) => serde_json::Value::String(s.clone()),
653            Value::Array(arr) => {
654                serde_json::Value::Array(arr.iter().map(|v| v.to_json()).collect())
655            }
656            Value::Object(obj) => {
657                let map: serde_json::Map<String, serde_json::Value> =
658                    obj.iter().map(|(k, v)| (k.clone(), v.to_json())).collect();
659                serde_json::Value::Object(map)
660            }
661            Value::BlockRef(id) => serde_json::json!({"$ref": id}),
662        }
663    }
664}
665
666/// Condition for WHERE clauses
667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
668pub enum Condition {
669    Comparison {
670        path: Path,
671        op: ComparisonOp,
672        value: Value,
673    },
674    Contains {
675        path: Path,
676        value: Value,
677    },
678    StartsWith {
679        path: Path,
680        prefix: String,
681    },
682    EndsWith {
683        path: Path,
684        suffix: String,
685    },
686    Matches {
687        path: Path,
688        regex: String,
689    },
690    Exists {
691        path: Path,
692    },
693    IsNull {
694        path: Path,
695    },
696    And(Box<Condition>, Box<Condition>),
697    Or(Box<Condition>, Box<Condition>),
698    Not(Box<Condition>),
699}
700
701/// Comparison operator
702#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
703pub enum ComparisonOp {
704    Eq, // =
705    Ne, // !=
706    Gt, // >
707    Ge, // >=
708    Lt, // <
709    Le, // <=
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn test_path_simple() {
718        let path = Path::simple("content.text");
719        assert_eq!(path.segments.len(), 1);
720    }
721
722    #[test]
723    fn test_value_to_json() {
724        let value = Value::Object(
725            [("key".to_string(), Value::String("value".to_string()))]
726                .into_iter()
727                .collect(),
728        );
729        let json = value.to_json();
730        assert_eq!(json["key"], "value");
731    }
732}