Skip to main content

hirn_query/parser/
parse.rs

1//! HirnQL parser — transforms query text into AST.
2
3use pest::Parser;
4use pest_derive::Parser;
5
6use hirn_core::types::Layer;
7
8use super::ast::*;
9
10// ── Pest parser definition ─────────────────────────────────────────────
11
12#[derive(Parser)]
13#[grammar = "parser/hirnql.pest"]
14struct HirnQlParser;
15
16// ── Public API ─────────────────────────────────────────────────────────
17
18/// Parse error with location and context.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ParseError {
21    pub message: String,
22    pub line: usize,
23    pub column: usize,
24}
25
26impl std::fmt::Display for ParseError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "parse error at {}:{}: {}",
31            self.line, self.column, self.message
32        )
33    }
34}
35
36impl std::error::Error for ParseError {}
37
38impl ParseError {
39    /// Shorthand for errors without precise location (line/column default to 1).
40    fn simple(message: impl Into<String>) -> Self {
41        Self {
42            message: message.into(),
43            line: 1,
44            column: 1,
45        }
46    }
47}
48
49/// Configurable limits for query parsing.
50#[derive(Debug, Clone)]
51pub struct QueryLimits {
52    /// Maximum query string length in bytes (default: 1 MB).
53    pub max_query_length: usize,
54    /// Maximum EXPAND GRAPH DEPTH value (default: 10).
55    pub max_expand_depth: usize,
56    /// Maximum LIMIT value (default: 10,000).
57    pub max_limit: usize,
58    /// Maximum BUDGET (context token budget) value (default: 1,000,000).
59    ///
60    /// Prevents runaway context budgets that could exhaust memory.
61    pub max_context_budget: usize,
62    /// Maximum MODE ITERATIVE MAX_HOPS value (default: 5).
63    ///
64    /// Prevents clients from escalating beyond the validated iterative-retrieval
65    /// hop ceiling — enforces the same cap regardless of operator-level default.
66    pub max_iterative_hops: usize,
67}
68
69impl Default for QueryLimits {
70    fn default() -> Self {
71        Self {
72            max_query_length: 1_048_576, // 1 MB
73            max_expand_depth: 10,
74            max_limit: 10_000,
75            max_context_budget: 1_000_000,
76            max_iterative_hops: 5,
77        }
78    }
79}
80
81/// Parse a HirnQL query string into a `Statement`.
82pub fn parse(input: &str) -> Result<Statement, ParseError> {
83    parse_with_limits(input, &QueryLimits::default())
84}
85
86/// Parse a HirnQL query string with configurable limits.
87pub fn parse_with_limits(input: &str, limits: &QueryLimits) -> Result<Statement, ParseError> {
88    if input.len() > limits.max_query_length {
89        return Err(ParseError::simple(format!(
90            "query too large: {} bytes exceeds maximum of {} bytes",
91            input.len(),
92            limits.max_query_length
93        )));
94    }
95
96    let pairs = HirnQlParser::parse(Rule::statement, input).map_err(|e| {
97        let (line, col) = match e.line_col {
98            pest::error::LineColLocation::Pos((l, c)) => (l, c),
99            pest::error::LineColLocation::Span((l, c), _) => (l, c),
100        };
101
102        let msg = format_pest_error(&e, input);
103        ParseError {
104            message: msg,
105            line,
106            column: col,
107        }
108    })?;
109
110    let statement_pair = pairs
111        .into_iter()
112        .next()
113        .ok_or_else(|| ParseError::simple("empty input"))?;
114
115    let stmt = build_statement(statement_pair)?;
116    validate_limits(&stmt, limits)?;
117    Ok(stmt)
118}
119
120/// Validate parsed statement against configured limits.
121fn validate_limits(stmt: &Statement, limits: &QueryLimits) -> Result<(), ParseError> {
122    match stmt {
123        Statement::Recall(r) => {
124            if let Some(limit) = r.limit {
125                check_limit(limit, limits.max_limit)?;
126            }
127            if let Some(budget) = r.budget {
128                check_budget(budget, limits.max_context_budget)?;
129            }
130            if let Some(ref expand) = r.expand {
131                check_depth(expand.depth, limits.max_expand_depth)?;
132            }
133        }
134        Statement::Think(t) => {
135            if let Some(limit) = t.limit {
136                check_limit(limit, limits.max_limit)?;
137            }
138            if let Some(budget) = t.budget {
139                check_budget(budget, limits.max_context_budget)?;
140            }
141            if let Some(hops) = t.max_hops {
142                check_max_hops(hops, limits.max_iterative_hops)?;
143            }
144            if let Some(ref expand) = t.expand {
145                check_depth(expand.depth, limits.max_expand_depth)?;
146            }
147        }
148        Statement::RecallEvents(r) => {
149            if let Some(limit) = r.limit {
150                check_limit(limit, limits.max_limit)?;
151            }
152        }
153        Statement::Traverse(t) => {
154            check_depth(t.depth, limits.max_expand_depth)?;
155            if let Some(limit) = t.limit {
156                check_limit(limit, limits.max_limit)?;
157            }
158        }
159        Statement::Explain(e) => validate_limits(&e.inner, limits)?,
160        _ => {}
161    }
162    Ok(())
163}
164
165fn check_limit(value: usize, max: usize) -> Result<(), ParseError> {
166    if value > max {
167        return Err(ParseError::simple(format!(
168            "LIMIT {value} exceeds maximum allowed value of {max}"
169        )));
170    }
171    Ok(())
172}
173
174fn check_depth(value: usize, max: usize) -> Result<(), ParseError> {
175    if value > max {
176        return Err(ParseError::simple(format!(
177            "DEPTH {value} exceeds maximum allowed value of {max}"
178        )));
179    }
180    Ok(())
181}
182
183fn check_budget(value: usize, max: usize) -> Result<(), ParseError> {
184    if value > max {
185        return Err(ParseError::simple(format!(
186            "BUDGET {value} exceeds maximum allowed value of {max}"
187        )));
188    }
189    Ok(())
190}
191
192fn check_max_hops(value: usize, max: usize) -> Result<(), ParseError> {
193    if value > max {
194        return Err(ParseError::simple(format!(
195            "MAX_HOPS {value} exceeds maximum allowed value of {max}"
196        )));
197    }
198    Ok(())
199}
200
201/// Format pest errors into user-friendly messages.
202fn format_pest_error(e: &pest::error::Error<Rule>, input: &str) -> String {
203    // Detect common error patterns and provide helpful suggestions.
204    let base = e.variant.message().to_string();
205
206    let trimmed = input.trim();
207    if let Some(first_word) = trimmed.split_whitespace().next() {
208        let upper = first_word.to_uppercase();
209        let known = [
210            "RECALL",
211            "THINK",
212            "REMEMBER",
213            "FORGET",
214            "CORRECT",
215            "SUPERSEDE",
216            "RETRACT",
217            "CONNECT",
218            "INSPECT",
219            "HISTORY",
220            "TRACE",
221            "CONSOLIDATE",
222            "WATCH",
223            "TRAVERSE",
224            "EXPLAIN",
225            "CREATE",
226            "DROP",
227            "GRANT",
228            "REVOKE",
229            "SHOW",
230        ];
231        if !known.contains(&upper.as_str()) {
232            return format!("unknown verb '{first_word}', did you mean 'RECALL'?");
233        }
234    }
235
236    base
237}
238
239// ── AST construction ───────────────────────────────────────────────────
240
241fn unsupported_embedded_statement(rule: Rule) -> Result<Statement, ParseError> {
242    Err(ParseError::simple(match rule {
243        Rule::remember_stmt => {
244            "REMEMBER is not supported via embedded HirnQL anymore; use the direct memory view APIs instead"
245        }
246        Rule::forget_stmt => {
247            "FORGET is not supported via embedded HirnQL anymore; use the direct memory view APIs instead"
248        }
249        Rule::connect_stmt => {
250            "CONNECT is not supported via embedded HirnQL anymore; use the graph view APIs instead"
251        }
252        Rule::consolidate_stmt => {
253            "CONSOLIDATE is not supported via HirnQL anymore; use db.admin().consolidate().execute() instead"
254        }
255        Rule::watch_stmt => {
256            "WATCH is not supported via embedded HirnQL anymore; use the event or daemon APIs instead"
257        }
258        _ => "statement is not supported via embedded HirnQL anymore",
259    }))
260}
261
262fn build_statement(pair: pest::iterators::Pair<'_, Rule>) -> Result<Statement, ParseError> {
263    let inner = pair
264        .into_inner()
265        .next()
266        .ok_or_else(|| ParseError::simple("empty statement"))?;
267
268    match inner.as_rule() {
269        Rule::recall_events_stmt => Ok(Statement::RecallEvents(build_recall_events(inner)?)),
270        Rule::recall_stmt => build_recall(inner).map(|stmt| Statement::Recall(Box::new(stmt))),
271        Rule::think_stmt => build_think(inner).map(|stmt| Statement::Think(Box::new(stmt))),
272        Rule::remember_stmt => unsupported_embedded_statement(Rule::remember_stmt),
273        Rule::forget_stmt => unsupported_embedded_statement(Rule::forget_stmt),
274        Rule::correct_stmt => build_correct(inner).map(Statement::Correct),
275        Rule::supersede_stmt => build_supersede(inner).map(Statement::Supersede),
276        Rule::merge_memory_stmt => build_merge_memory(inner).map(Statement::MergeMemory),
277        Rule::retract_stmt => build_retract(inner).map(Statement::Retract),
278        Rule::connect_stmt => unsupported_embedded_statement(Rule::connect_stmt),
279        Rule::inspect_stmt => Ok(Statement::Inspect(build_inspect(inner)?)),
280        Rule::history_stmt => Ok(Statement::History(build_history(inner)?)),
281        Rule::trace_stmt => Ok(Statement::Trace(build_trace(inner)?)),
282        Rule::consolidate_stmt => unsupported_embedded_statement(Rule::consolidate_stmt),
283        Rule::watch_stmt => unsupported_embedded_statement(Rule::watch_stmt),
284        Rule::traverse_stmt => build_traverse(inner).map(Statement::Traverse),
285        Rule::explain_stmt => build_explain(inner),
286        Rule::explain_causes_stmt => build_explain_causes(inner).map(Statement::ExplainCauses),
287        Rule::what_if_stmt => build_what_if(inner).map(Statement::WhatIf),
288        Rule::counterfactual_stmt => build_counterfactual(inner).map(Statement::Counterfactual),
289        Rule::create_realm_stmt => Ok(Statement::CreateRealm(build_create_realm(inner)?)),
290        Rule::drop_realm_stmt => Ok(Statement::DropRealm(build_drop_realm(inner)?)),
291        Rule::grant_stmt => build_grant(inner).map(Statement::Grant),
292        Rule::revoke_stmt => build_revoke(inner).map(Statement::Revoke),
293        Rule::show_policies_stmt => Ok(Statement::ShowPolicies(build_show_policies(inner)?)),
294        Rule::explain_policy_stmt => build_explain_policy(inner).map(Statement::ExplainPolicy),
295        Rule::show_cluster_stmt => Ok(Statement::ShowCluster),
296        Rule::set_tier_policy_stmt => Ok(Statement::SetTierPolicy(build_set_tier_policy(inner)?)),
297        _ => Err(ParseError::simple(format!(
298            "unexpected rule: {:?}",
299            inner.as_rule()
300        ))),
301    }
302}
303
304fn build_recall(pair: pest::iterators::Pair<'_, Rule>) -> Result<RecallStmt, ParseError> {
305    let mut stmt = RecallStmt {
306        layers: vec![],
307        about: String::new(),
308        involving: None,
309        temporal: None,
310        as_of: None,
311        expand: None,
312        follow_causes: None,
313        where_clauses: vec![],
314        subquery_filters: vec![],
315        modality: None,
316        resource_roles: None,
317        hydration_modes: None,
318        artifact_kinds: None,
319        depth_mode: None,
320        with_prospective: None,
321        with_mcfa: None,
322        with_conflicts: false,
323        provenance_depth: None,
324        topic: None,
325        group_by: None,
326        projection: None,
327        output_format: None,
328        result_format: None,
329        budget: None,
330        namespace: None,
331        from_realms: None,
332        consistency: None,
333        limit: None,
334        hybrid: false,
335    };
336
337    for inner in pair.into_inner() {
338        match inner.as_rule() {
339            Rule::layer_filter => stmt.layers = build_layer_filter(inner),
340            Rule::about_clause => stmt.about = extract_about(inner)?,
341            Rule::involving_clause => stmt.involving = Some(extract_string_list(inner)?),
342            Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
343            Rule::as_of_clause => stmt.as_of = Some(build_as_of(inner)?),
344            Rule::expand_clause => stmt.expand = Some(build_expand(inner)?),
345            Rule::follow_causes_clause => stmt.follow_causes = Some(extract_follow_causes(inner)?),
346            Rule::where_clause => {
347                // where_clause can contain either a condition or in_subquery_condition
348                let child = inner
349                    .into_inner()
350                    .next()
351                    .ok_or_else(|| ParseError::simple("empty WHERE clause"))?;
352                match child.as_rule() {
353                    Rule::in_subquery_condition => {
354                        stmt.subquery_filters.push(build_in_subquery(child)?);
355                    }
356                    Rule::condition => {
357                        stmt.where_clauses.push(build_condition(child)?);
358                    }
359                    _ => {}
360                }
361            }
362            Rule::group_by_clause => stmt.group_by = Some(build_group_by(inner)),
363            Rule::select_clause => stmt.projection = Some(build_field_list(inner)),
364            Rule::as_clause => stmt.output_format = Some(build_output_format(inner)),
365            Rule::format_clause => stmt.result_format = Some(build_format_clause(inner)),
366            Rule::budget_clause => stmt.budget = Some(extract_budget(inner)?),
367            Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
368            Rule::from_realm_clause => stmt.from_realms = Some(extract_realm_list(inner)),
369            Rule::consistency_clause => stmt.consistency = Some(build_consistency(inner)),
370            Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
371            Rule::modality_clause => stmt.modality = Some(build_modality_list(inner)?),
372            Rule::resource_role_clause => {
373                stmt.resource_roles = Some(build_evidence_role_list(inner)?);
374            }
375            Rule::hydration_clause => {
376                stmt.hydration_modes = Some(build_hydration_mode_list(inner)?);
377            }
378            Rule::artifact_clause => {
379                stmt.artifact_kinds = Some(build_artifact_kind_list(inner)?);
380            }
381            Rule::depth_clause => stmt.depth_mode = Some(build_depth_mode(inner)?),
382            Rule::topic_clause => stmt.topic = Some(extract_string_from_clause(inner)?),
383            Rule::with_prospective_clause => stmt.with_prospective = Some(build_on_off(inner)?),
384            Rule::with_mcfa_clause => stmt.with_mcfa = Some(build_on_off(inner)?),
385            Rule::with_conflicts_clause => stmt.with_conflicts = true,
386            Rule::with_provenance_clause => {
387                stmt.provenance_depth = Some(extract_integer_from_clause(inner)?);
388            }
389            Rule::hybrid_clause => stmt.hybrid = true,
390            _ => {}
391        }
392    }
393
394    if stmt.about.is_empty() {
395        return Err(ParseError::simple("RECALL requires ABOUT clause"));
396    }
397
398    Ok(stmt)
399}
400
401fn build_recall_events(
402    pair: pest::iterators::Pair<'_, Rule>,
403) -> Result<RecallEventsStmt, ParseError> {
404    let mut stmt = RecallEventsStmt {
405        entity_filter: None,
406        where_clauses: vec![],
407        temporal: None,
408        namespace: None,
409        limit: None,
410    };
411
412    for inner in pair.into_inner() {
413        match inner.as_rule() {
414            Rule::events_for_clause => {
415                stmt.entity_filter = Some(extract_string_from_clause(inner)?);
416            }
417            Rule::where_clause => stmt.where_clauses.push(build_where(inner)?),
418            Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
419            Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
420            Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
421            _ => {}
422        }
423    }
424
425    Ok(stmt)
426}
427
428fn build_think(pair: pest::iterators::Pair<'_, Rule>) -> Result<ThinkStmt, ParseError> {
429    let mut stmt = ThinkStmt {
430        about: String::new(),
431        involving: None,
432        temporal: None,
433        expand: None,
434        follow_causes: None,
435        where_clauses: vec![],
436        output_format: None,
437        budget: None,
438        namespace: None,
439        consistency: None,
440        limit: None,
441        hybrid: false,
442        mode: RetrievalMode::Local,
443        depth_mode: None,
444        with_prospective: None,
445        with_mcfa: None,
446        provenance_depth: None,
447        max_hops: None,
448        community_depth: None,
449    };
450
451    for inner in pair.into_inner() {
452        match inner.as_rule() {
453            Rule::about_clause => stmt.about = extract_about(inner)?,
454            Rule::involving_clause => stmt.involving = Some(extract_string_list(inner)?),
455            Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
456            Rule::expand_clause => stmt.expand = Some(build_expand(inner)?),
457            Rule::follow_causes_clause => stmt.follow_causes = Some(extract_follow_causes(inner)?),
458            Rule::where_clause => stmt.where_clauses.push(build_where(inner)?),
459            Rule::as_clause => stmt.output_format = Some(build_output_format(inner)),
460            Rule::budget_clause => stmt.budget = Some(extract_budget(inner)?),
461            Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
462            Rule::consistency_clause => stmt.consistency = Some(build_consistency(inner)),
463            Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
464            Rule::global_clause => stmt.mode = RetrievalMode::Global,
465            Rule::mode_clause => {
466                let (mode, max_hops) = build_retrieval_mode_with_hops(inner)?;
467                stmt.mode = mode;
468                if max_hops.is_some() {
469                    stmt.max_hops = max_hops;
470                }
471            }
472            Rule::hybrid_clause => stmt.hybrid = true,
473            Rule::depth_clause => stmt.depth_mode = Some(build_depth_mode(inner)?),
474            Rule::with_prospective_clause => stmt.with_prospective = Some(build_on_off(inner)?),
475            Rule::with_mcfa_clause => stmt.with_mcfa = Some(build_on_off(inner)?),
476            Rule::with_provenance_clause => {
477                stmt.provenance_depth = Some(extract_integer_from_clause(inner)?);
478            }
479            Rule::community_depth_clause => {
480                stmt.community_depth = Some(extract_integer_from_clause(inner)?);
481            }
482            _ => {}
483        }
484    }
485
486    Ok(stmt)
487}
488
489fn build_correct(pair: pest::iterators::Pair<'_, Rule>) -> Result<CorrectStmt, ParseError> {
490    let mut target = None;
491    let mut updates = Vec::new();
492    let mut reason = None;
493    let mut observed_at = None;
494    let mut caused_by = None;
495    let mut namespace = None;
496
497    for inner in pair.into_inner() {
498        match inner.as_rule() {
499            Rule::semantic_target_ref if target.is_none() => {
500                target = Some(build_semantic_target_ref(inner)?);
501            }
502            Rule::set_assignment_list => updates = build_set_assignment_list(inner)?,
503            Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
504            Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
505            Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
506            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
507            _ => {}
508        }
509    }
510
511    Ok(CorrectStmt {
512        target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
513        updates,
514        reason,
515        observed_at,
516        caused_by,
517        namespace,
518    })
519}
520
521fn build_supersede(pair: pest::iterators::Pair<'_, Rule>) -> Result<SupersedeStmt, ParseError> {
522    let mut target = None;
523    let mut updates = Vec::new();
524    let mut reason = None;
525    let mut observed_at = None;
526    let mut caused_by = None;
527    let mut namespace = None;
528
529    for inner in pair.into_inner() {
530        match inner.as_rule() {
531            Rule::semantic_target_ref if target.is_none() => {
532                target = Some(build_semantic_target_ref(inner)?);
533            }
534            Rule::set_assignment_list => updates = build_set_assignment_list(inner)?,
535            Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
536            Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
537            Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
538            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
539            _ => {}
540        }
541    }
542
543    Ok(SupersedeStmt {
544        target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
545        updates,
546        reason,
547        observed_at,
548        caused_by,
549        namespace,
550    })
551}
552
553fn build_merge_memory(
554    pair: pest::iterators::Pair<'_, Rule>,
555) -> Result<MergeMemoryStmt, ParseError> {
556    let mut sources = Vec::new();
557    let mut target = None;
558    let mut updates = Vec::new();
559    let mut reason = None;
560    let mut observed_at = None;
561    let mut caused_by = None;
562    let mut namespace = None;
563
564    for inner in pair.into_inner() {
565        match inner.as_rule() {
566            Rule::semantic_target_list if sources.is_empty() => {
567                sources = build_semantic_target_list(inner)?;
568            }
569            Rule::semantic_target_ref if target.is_none() => {
570                target = Some(build_semantic_target_ref(inner)?);
571            }
572            Rule::merge_set_clause => {
573                for child in inner.into_inner() {
574                    if child.as_rule() == Rule::set_assignment_list {
575                        updates = build_set_assignment_list(child)?;
576                    }
577                }
578            }
579            Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
580            Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
581            Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
582            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
583            _ => {}
584        }
585    }
586
587    Ok(MergeMemoryStmt {
588        sources,
589        target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
590        updates,
591        reason,
592        observed_at,
593        caused_by,
594        namespace,
595    })
596}
597
598fn build_retract(pair: pest::iterators::Pair<'_, Rule>) -> Result<RetractStmt, ParseError> {
599    let mut target = None;
600    let mut reason = None;
601    let mut observed_at = None;
602    let mut caused_by = None;
603    let mut namespace = None;
604
605    for inner in pair.into_inner() {
606        match inner.as_rule() {
607            Rule::semantic_target_ref if target.is_none() => {
608                target = Some(build_semantic_target_ref(inner)?);
609            }
610            Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
611            Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
612            Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
613            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
614            _ => {}
615        }
616    }
617
618    Ok(RetractStmt {
619        target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
620        reason,
621        observed_at,
622        caused_by,
623        namespace,
624    })
625}
626
627fn build_inspect(pair: pest::iterators::Pair<'_, Rule>) -> Result<InspectStmt, ParseError> {
628    let target = pair
629        .into_inner()
630        .find(|p| p.as_rule() == Rule::semantic_target_ref)
631        .map(build_semantic_target_ref)
632        .transpose()?
633        .unwrap_or_else(|| SemanticTargetRef::Memory(String::new()));
634    Ok(InspectStmt { target })
635}
636
637fn build_history(pair: pest::iterators::Pair<'_, Rule>) -> Result<HistoryStmt, ParseError> {
638    let mut target = None;
639    let mut namespace = None;
640
641    for inner in pair.into_inner() {
642        match inner.as_rule() {
643            Rule::semantic_target_ref if target.is_none() => {
644                target = Some(build_semantic_target_ref(inner)?);
645            }
646            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
647            _ => {}
648        }
649    }
650
651    Ok(HistoryStmt {
652        target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
653        namespace,
654    })
655}
656
657fn build_trace(pair: pest::iterators::Pair<'_, Rule>) -> Result<TraceStmt, ParseError> {
658    let target = pair
659        .into_inner()
660        .find(|p| p.as_rule() == Rule::semantic_target_ref)
661        .map(build_semantic_target_ref)
662        .transpose()?
663        .unwrap_or_else(|| SemanticTargetRef::Memory(String::new()));
664    Ok(TraceStmt { target })
665}
666
667fn build_semantic_target_list(
668    pair: pest::iterators::Pair<'_, Rule>,
669) -> Result<Vec<SemanticTargetRef>, ParseError> {
670    pair.into_inner()
671        .filter(|child| child.as_rule() == Rule::semantic_target_ref)
672        .map(build_semantic_target_ref)
673        .collect()
674}
675
676fn build_semantic_target_ref(
677    pair: pest::iterators::Pair<'_, Rule>,
678) -> Result<SemanticTargetRef, ParseError> {
679    let Some(inner) = pair.into_inner().next() else {
680        return Ok(SemanticTargetRef::Memory(String::new()));
681    };
682
683    match inner.as_rule() {
684        Rule::logical_target_ref => {
685            let value = inner
686                .into_inner()
687                .find(|child| child.as_rule() == Rule::string_literal)
688                .map(extract_string_value)
689                .transpose()?
690                .unwrap_or_default();
691            Ok(SemanticTargetRef::Logical(value))
692        }
693        Rule::revision_target_ref => {
694            let value = inner
695                .into_inner()
696                .find(|child| child.as_rule() == Rule::string_literal)
697                .map(extract_string_value)
698                .transpose()?
699                .unwrap_or_default();
700            Ok(SemanticTargetRef::Revision(value))
701        }
702        Rule::string_literal => Ok(SemanticTargetRef::Memory(extract_string_value(inner)?)),
703        _ => Err(ParseError::simple(format!(
704            "unexpected semantic target rule: {:?}",
705            inner.as_rule()
706        ))),
707    }
708}
709
710// ── Clause builders ────────────────────────────────────────────────────
711
712fn build_layer_filter(pair: pest::iterators::Pair<'_, Rule>) -> Vec<Layer> {
713    pair.into_inner()
714        .filter(|p| p.as_rule() == Rule::layer_name)
715        .map(|p| {
716            let s = p.as_str();
717            if s.eq_ignore_ascii_case("episodic") {
718                Layer::Episodic
719            } else if s.eq_ignore_ascii_case("semantic") {
720                Layer::Semantic
721            } else if s.eq_ignore_ascii_case("working") {
722                Layer::Working
723            } else if s.eq_ignore_ascii_case("procedural") {
724                Layer::Procedural
725            } else {
726                Layer::Episodic
727            }
728        })
729        .collect()
730}
731
732fn extract_about(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
733    let inner = pair
734        .into_inner()
735        .next()
736        .ok_or_else(|| ParseError::simple("empty ABOUT clause"))?;
737    Ok(match inner.as_rule() {
738        Rule::parameter => inner.as_str().to_string(),
739        Rule::string_literal => extract_string_value(inner)?,
740        _ => String::new(),
741    })
742}
743
744fn extract_string_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
745    fn inner_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
746        let mut result = Vec::new();
747        for p in pair.into_inner() {
748            if p.as_rule() == Rule::string_list {
749                result.extend(inner_list(p)?);
750            } else if p.as_rule() == Rule::string_literal {
751                result.push(extract_string_value(p)?);
752            }
753        }
754        Ok(result)
755    }
756    inner_list(pair)
757}
758
759fn extract_string_value(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
760    // string_literal → double_inner | single_inner (containing plain chars + escape_seq)
761    let raw = pair
762        .into_inner()
763        .next()
764        .map(|p| p.as_str().to_string())
765        .unwrap_or_default();
766    unescape_string(&raw)
767}
768
769/// Process escape sequences (\\, \", \', \n, \t, \r) in a parsed string.
770/// Returns an error for unrecognised escape sequences.
771fn unescape_string(s: &str) -> Result<String, ParseError> {
772    let mut out = String::with_capacity(s.len());
773    let mut chars = s.chars();
774    while let Some(c) = chars.next() {
775        if c == '\\' {
776            match chars.next() {
777                Some('n') => out.push('\n'),
778                Some('t') => out.push('\t'),
779                Some('r') => out.push('\r'),
780                Some('\\') => out.push('\\'),
781                Some('"') => out.push('"'),
782                Some('\'') => out.push('\''),
783                Some(other) => {
784                    return Err(ParseError::simple(format!(
785                        "invalid escape sequence: '\\{other}'"
786                    )));
787                }
788                None => out.push('\\'),
789            }
790        } else {
791            out.push(c);
792        }
793    }
794    Ok(out)
795}
796
797fn extract_string_from_clause(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
798    for p in pair.into_inner() {
799        match p.as_rule() {
800            Rule::parameter => return Ok(p.as_str().to_string()),
801            Rule::string_literal => return extract_string_value(p),
802            _ => {}
803        }
804    }
805    Ok(String::new())
806}
807
808fn extract_float(pair: pest::iterators::Pair<'_, Rule>) -> Result<f32, ParseError> {
809    let p = pair
810        .into_inner()
811        .find(|p| p.as_rule() == Rule::float_literal)
812        .ok_or_else(|| ParseError::simple("expected float literal"))?;
813    let text = p.as_str();
814    text.parse::<f32>()
815        .map_err(|_| ParseError::simple(format!("invalid float literal: '{text}'")))
816}
817
818fn extract_int(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
819    let p = pair
820        .into_inner()
821        .find(|p| p.as_rule() == Rule::integer_literal || p.as_rule() == Rule::parameter)
822        .ok_or_else(|| ParseError::simple("expected integer literal"))?;
823    // Parameters use a placeholder value; the real value is substituted after bind().
824    if p.as_rule() == Rule::parameter {
825        return Ok(0);
826    }
827    let text = p.as_str();
828    text.parse::<usize>()
829        .map_err(|_| ParseError::simple(format!("invalid integer literal: '{text}'")))
830}
831
832fn build_temporal(pair: pest::iterators::Pair<'_, Rule>) -> Result<TemporalClause, ParseError> {
833    let inner = pair
834        .into_inner()
835        .next()
836        .ok_or_else(|| ParseError::simple("empty temporal clause"))?;
837    Ok(match inner.as_rule() {
838        Rule::after_clause => {
839            let s = inner
840                .into_inner()
841                .find(|p| p.as_rule() == Rule::string_literal)
842                .map(extract_string_value)
843                .transpose()?
844                .unwrap_or_default();
845            TemporalClause::After(s)
846        }
847        Rule::before_clause => {
848            let s = inner
849                .into_inner()
850                .find(|p| p.as_rule() == Rule::string_literal)
851                .map(extract_string_value)
852                .transpose()?
853                .unwrap_or_default();
854            TemporalClause::Before(s)
855        }
856        Rule::between_clause => {
857            let strings: Vec<String> = inner
858                .into_inner()
859                .filter(|p| p.as_rule() == Rule::string_literal)
860                .map(extract_string_value)
861                .collect::<Result<Vec<_>, _>>()?;
862            TemporalClause::Between {
863                start: strings.first().cloned().unwrap_or_default(),
864                end: strings.get(1).cloned().unwrap_or_default(),
865            }
866        }
867        _ => TemporalClause::After(String::new()),
868    })
869}
870
871fn build_expand(pair: pest::iterators::Pair<'_, Rule>) -> Result<ExpandClause, ParseError> {
872    let mut depth = 1;
873    let mut min_weight = None;
874    let mut activation = None;
875
876    for inner in pair.into_inner() {
877        match inner.as_rule() {
878            Rule::integer_literal => {
879                depth = inner.as_str().parse::<usize>().map_err(|_| {
880                    ParseError::simple(format!("invalid DEPTH value: '{}'", inner.as_str()))
881                })?;
882            }
883            Rule::min_weight_clause => min_weight = Some(extract_float(inner)?),
884            Rule::activation_clause => {
885                let mode_str = inner
886                    .into_inner()
887                    .find(|p| p.as_rule() == Rule::activation_mode)
888                    .map(|p| p.as_str())
889                    .unwrap_or_default();
890                activation = Some(if mode_str.eq_ignore_ascii_case("spreading") {
891                    ActivationModeAst::Spreading
892                } else if mode_str.eq_ignore_ascii_case("static") {
893                    ActivationModeAst::Static
894                } else if mode_str.eq_ignore_ascii_case("ppr")
895                    || mode_str.eq_ignore_ascii_case("pagerank")
896                {
897                    ActivationModeAst::Ppr
898                } else {
899                    ActivationModeAst::None
900                });
901            }
902            _ => {}
903        }
904    }
905
906    Ok(ExpandClause {
907        depth,
908        min_weight,
909        activation,
910    })
911}
912
913fn extract_follow_causes(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
914    let p = pair
915        .into_inner()
916        .find(|p| p.as_rule() == Rule::integer_literal)
917        .ok_or_else(|| ParseError::simple("FOLLOW CAUSES requires an integer depth"))?;
918    let text = p.as_str();
919    text.parse::<usize>()
920        .map_err(|_| ParseError::simple(format!("invalid FOLLOW CAUSES depth: '{text}'")))
921}
922
923fn build_where(pair: pest::iterators::Pair<'_, Rule>) -> Result<WhereCondition, ParseError> {
924    let condition = pair
925        .into_inner()
926        .find(|p| p.as_rule() == Rule::condition)
927        .ok_or_else(|| ParseError::simple("WHERE clause missing condition"))?;
928    build_condition(condition)
929}
930
931/// Parse a single condition (field op value).
932fn build_condition(
933    condition: pest::iterators::Pair<'_, Rule>,
934) -> Result<WhereCondition, ParseError> {
935    let mut field = String::new();
936    let mut op = ComparisonOp::Gt;
937    let mut value = ConditionValue::Float(0.0);
938
939    for inner in condition.into_inner() {
940        match inner.as_rule() {
941            Rule::identifier => field = inner.as_str().to_string(),
942            Rule::comparison_op => {
943                op = match inner.as_str() {
944                    ">=" => ComparisonOp::Gte,
945                    "<=" => ComparisonOp::Lte,
946                    "!=" => ComparisonOp::Neq,
947                    ">" => ComparisonOp::Gt,
948                    "<" => ComparisonOp::Lt,
949                    "=" => ComparisonOp::Eq,
950                    _ => ComparisonOp::Eq,
951                };
952            }
953            Rule::float_literal => {
954                let text = inner.as_str();
955                value = ConditionValue::Float(text.parse().map_err(|_| {
956                    ParseError::simple(format!("invalid float in WHERE: '{text}'"))
957                })?);
958            }
959            Rule::integer_literal => {
960                let text = inner.as_str();
961                value = ConditionValue::Int(text.parse().map_err(|_| {
962                    ParseError::simple(format!("invalid integer in WHERE: '{text}'"))
963                })?);
964            }
965            Rule::string_literal => {
966                value = ConditionValue::String(extract_string_value(inner)?);
967            }
968            Rule::parameter => {
969                value = ConditionValue::Param(inner.as_str().to_string());
970            }
971            _ => {}
972        }
973    }
974
975    Ok(WhereCondition { field, op, value })
976}
977
978/// Parse an IN subquery condition: `field IN (RECALL ...)`.
979fn build_in_subquery(pair: pest::iterators::Pair<'_, Rule>) -> Result<SubqueryFilter, ParseError> {
980    let mut field = String::new();
981    let mut subquery = None;
982
983    for inner in pair.into_inner() {
984        match inner.as_rule() {
985            Rule::identifier => field = inner.as_str().to_string(),
986            Rule::subquery => subquery = Some(build_subquery(inner)?),
987            _ => {}
988        }
989    }
990
991    Ok(SubqueryFilter {
992        field,
993        subquery: subquery.unwrap_or(Subquery {
994            layers: vec![],
995            about: String::new(),
996            involving: None,
997            temporal: None,
998            limit: None,
999        }),
1000    })
1001}
1002
1003/// Parse the inner subquery (RECALL layer ABOUT "..." ...).
1004fn build_subquery(pair: pest::iterators::Pair<'_, Rule>) -> Result<Subquery, ParseError> {
1005    let mut layers = vec![];
1006    let mut about = String::new();
1007    let mut involving = None;
1008    let mut temporal = None;
1009    let mut limit = None;
1010
1011    for inner in pair.into_inner() {
1012        match inner.as_rule() {
1013            Rule::layer_filter => layers = build_layer_filter(inner),
1014            Rule::about_clause => about = extract_about(inner)?,
1015            Rule::involving_clause => involving = Some(extract_string_list(inner)?),
1016            Rule::temporal_clause => temporal = Some(build_temporal(inner)?),
1017            Rule::limit_clause => limit = Some(extract_limit(inner)?),
1018            _ => {}
1019        }
1020    }
1021
1022    Ok(Subquery {
1023        layers,
1024        about,
1025        involving,
1026        temporal,
1027        limit,
1028    })
1029}
1030
1031/// Parse an AS OF clause for time-travel queries.
1032fn build_as_of(pair: pest::iterators::Pair<'_, Rule>) -> Result<RecallSnapshotAst, ParseError> {
1033    let inner = pair
1034        .into_inner()
1035        .next()
1036        .ok_or_else(|| ParseError::simple("AS OF clause requires a snapshot target"))?;
1037
1038    match inner.as_rule() {
1039        Rule::string_literal => Ok(RecallSnapshotAst::Unqualified(extract_string_value(inner)?)),
1040        Rule::as_of_observed => Ok(RecallSnapshotAst::Observed(extract_single_string_literal(
1041            inner,
1042            "AS OF OBSERVED",
1043        )?)),
1044        Rule::as_of_recorded => Ok(RecallSnapshotAst::Recorded(extract_single_string_literal(
1045            inner,
1046            "AS OF RECORDED",
1047        )?)),
1048        Rule::as_of_revision => Ok(RecallSnapshotAst::Revision(extract_single_string_literal(
1049            inner,
1050            "AS OF REVISION",
1051        )?)),
1052        other => Err(ParseError::simple(format!(
1053            "unexpected AS OF target: {other:?}"
1054        ))),
1055    }
1056}
1057
1058fn extract_single_string_literal(
1059    pair: pest::iterators::Pair<'_, Rule>,
1060    clause: &str,
1061) -> Result<String, ParseError> {
1062    pair.into_inner()
1063        .find(|p| p.as_rule() == Rule::string_literal)
1064        .map(extract_string_value)
1065        .transpose()?
1066        .ok_or_else(|| ParseError::simple(format!("{clause} requires a string literal")))
1067}
1068
1069fn build_output_format(pair: pest::iterators::Pair<'_, Rule>) -> OutputFormat {
1070    let fmt_str = pair
1071        .into_inner()
1072        .find(|p| p.as_rule() == Rule::output_format)
1073        .map(|p| p.as_str())
1074        .unwrap_or_default();
1075    parse_output_format(fmt_str)
1076}
1077
1078fn build_format_clause(pair: pest::iterators::Pair<'_, Rule>) -> OutputFormat {
1079    let fmt_str = pair
1080        .into_inner()
1081        .find(|p| p.as_rule() == Rule::output_format)
1082        .map(|p| p.as_str())
1083        .unwrap_or_default();
1084    parse_output_format(fmt_str)
1085}
1086
1087fn parse_output_format(s: &str) -> OutputFormat {
1088    if s.eq_ignore_ascii_case("narrative") {
1089        OutputFormat::Narrative
1090    } else if s.eq_ignore_ascii_case("context") {
1091        OutputFormat::Context
1092    } else if s.eq_ignore_ascii_case("graph") {
1093        OutputFormat::Graph
1094    } else if s.eq_ignore_ascii_case("causal_chain") {
1095        OutputFormat::CausalChain
1096    } else if s.eq_ignore_ascii_case("json") {
1097        OutputFormat::Json
1098    } else if s.eq_ignore_ascii_case("csv") {
1099        OutputFormat::Csv
1100    } else if s.eq_ignore_ascii_case("structured") {
1101        OutputFormat::Structured
1102    } else {
1103        OutputFormat::Context
1104    }
1105}
1106
1107fn build_group_by(pair: pest::iterators::Pair<'_, Rule>) -> GroupByClause {
1108    let mut field = String::new();
1109    let mut function = AggFunction::Count;
1110    for inner in pair.into_inner() {
1111        match inner.as_rule() {
1112            Rule::identifier => field = inner.as_str().to_string(),
1113            Rule::agg_function => {
1114                let s = inner.as_str();
1115                function = if s.eq_ignore_ascii_case("count") {
1116                    AggFunction::Count
1117                } else if s.eq_ignore_ascii_case("avg") {
1118                    AggFunction::Avg
1119                } else if s.eq_ignore_ascii_case("sum") {
1120                    AggFunction::Sum
1121                } else if s.eq_ignore_ascii_case("min") {
1122                    AggFunction::Min
1123                } else if s.eq_ignore_ascii_case("max") {
1124                    AggFunction::Max
1125                } else {
1126                    AggFunction::Count
1127                };
1128            }
1129            _ => {}
1130        }
1131    }
1132    GroupByClause { field, function }
1133}
1134
1135fn build_field_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1136    let mut fields = Vec::new();
1137    for inner in pair.into_inner() {
1138        if inner.as_rule() == Rule::field_list {
1139            for field in inner.into_inner() {
1140                if field.as_rule() == Rule::identifier {
1141                    fields.push(field.as_str().to_string());
1142                }
1143            }
1144        }
1145    }
1146    fields
1147}
1148
1149fn extract_budget(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1150    extract_int(pair)
1151}
1152
1153fn extract_namespace(pair: pest::iterators::Pair<'_, Rule>) -> String {
1154    pair.into_inner()
1155        .find_map(|p| match p.as_rule() {
1156            Rule::namespace_identifier => Some(p.as_str().to_string()),
1157            Rule::string_literal => extract_string_value(p).ok(),
1158            _ => None,
1159        })
1160        .unwrap_or_default()
1161}
1162
1163/// Extract realm IDs from `from_realm_clause`:
1164/// `FROM REALM "a", "b"` → `["a", "b"]`
1165fn extract_realm_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1166    pair.into_inner()
1167        .filter(|p| p.as_rule() == Rule::string_literal)
1168        .filter_map(|p| extract_string_value(p).ok())
1169        .collect()
1170}
1171
1172fn build_consistency(pair: pest::iterators::Pair<'_, Rule>) -> ConsistencyLevel {
1173    let level_str = pair
1174        .into_inner()
1175        .find(|p| p.as_rule() == Rule::consistency_level)
1176        .map(|p| p.as_str())
1177        .unwrap_or_default();
1178    if level_str.eq_ignore_ascii_case("linearizable") {
1179        ConsistencyLevel::Linearizable
1180    } else if level_str.eq_ignore_ascii_case("eventual") {
1181        ConsistencyLevel::Eventual
1182    } else {
1183        ConsistencyLevel::Session
1184    }
1185}
1186
1187fn extract_limit(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1188    extract_int(pair)
1189}
1190
1191fn build_retrieval_mode_with_hops(
1192    pair: pest::iterators::Pair<'_, Rule>,
1193) -> Result<(RetrievalMode, Option<usize>), ParseError> {
1194    let mut mode = RetrievalMode::Local;
1195    let mut max_hops = None;
1196
1197    for inner in pair.into_inner() {
1198        match inner.as_rule() {
1199            Rule::retrieval_mode => {
1200                let mode_str = inner.as_str();
1201                mode = if mode_str.eq_ignore_ascii_case("global") {
1202                    RetrievalMode::Global
1203                } else if mode_str.eq_ignore_ascii_case("hybrid") {
1204                    RetrievalMode::Hybrid
1205                } else if mode_str.eq_ignore_ascii_case("raptor") {
1206                    RetrievalMode::Raptor
1207                } else if mode_str.eq_ignore_ascii_case("adaptive") {
1208                    RetrievalMode::Adaptive
1209                } else if mode_str.eq_ignore_ascii_case("iterative") {
1210                    RetrievalMode::Iterative
1211                } else {
1212                    RetrievalMode::Local
1213                };
1214            }
1215            Rule::max_hops_clause => {
1216                let hops = extract_integer_from_clause(inner)?;
1217                if hops == 0 || hops > 5 {
1218                    return Err(ParseError::simple(format!(
1219                        "MAX_HOPS must be between 1 and 5, got {hops}"
1220                    )));
1221                }
1222                max_hops = Some(hops);
1223            }
1224            _ => {}
1225        }
1226    }
1227
1228    // MAX_HOPS is only valid with ITERATIVE mode.
1229    if max_hops.is_some() && mode != RetrievalMode::Iterative {
1230        return Err(ParseError::simple(
1231            "MAX_HOPS can only be used with MODE ITERATIVE",
1232        ));
1233    }
1234
1235    Ok((mode, max_hops))
1236}
1237
1238fn build_depth_mode(pair: pest::iterators::Pair<'_, Rule>) -> Result<DepthModeAst, ParseError> {
1239    let mode_str = pair
1240        .into_inner()
1241        .find(|p| p.as_rule() == Rule::depth_mode)
1242        .map(|p| p.as_str())
1243        .unwrap_or_default();
1244    if mode_str.eq_ignore_ascii_case("full") {
1245        Ok(DepthModeAst::Full)
1246    } else if mode_str.eq_ignore_ascii_case("summary") {
1247        Ok(DepthModeAst::Summary)
1248    } else if mode_str.eq_ignore_ascii_case("auto") {
1249        Ok(DepthModeAst::Auto)
1250    } else {
1251        Err(ParseError::simple(format!(
1252            "unknown DEPTH mode '{mode_str}', expected AUTO, FULL, or SUMMARY"
1253        )))
1254    }
1255}
1256
1257fn build_on_off(pair: pest::iterators::Pair<'_, Rule>) -> Result<bool, ParseError> {
1258    let val = pair
1259        .into_inner()
1260        .find(|p| p.as_rule() == Rule::on_off)
1261        .map(|p| p.as_str().to_string())
1262        .unwrap_or_default();
1263    if val.eq_ignore_ascii_case("on") {
1264        Ok(true)
1265    } else if val.eq_ignore_ascii_case("off") {
1266        Ok(false)
1267    } else {
1268        Err(ParseError::simple(format!(
1269            "expected ON or OFF, got '{val}'"
1270        )))
1271    }
1272}
1273
1274fn extract_integer_from_clause(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1275    extract_int(pair)
1276}
1277
1278fn build_traverse(pair: pest::iterators::Pair<'_, Rule>) -> Result<TraverseStmt, ParseError> {
1279    let mut from = String::new();
1280    let mut via = None;
1281    let mut depth = 1;
1282    let mut where_clauses = vec![];
1283    let mut limit = None;
1284
1285    for inner in pair.into_inner() {
1286        match inner.as_rule() {
1287            Rule::string_literal => from = extract_string_value(inner)?,
1288            Rule::via_clause => {
1289                let mut rels = vec![];
1290                for child in inner.into_inner() {
1291                    if child.as_rule() == Rule::relation_list {
1292                        for id in child.into_inner() {
1293                            if id.as_rule() == Rule::identifier {
1294                                rels.push(id.as_str().to_string());
1295                            }
1296                        }
1297                    }
1298                }
1299                via = Some(rels);
1300            }
1301            Rule::integer_literal => {
1302                depth = inner.as_str().parse::<usize>().map_err(|_| {
1303                    ParseError::simple(format!("invalid DEPTH value: '{}'", inner.as_str()))
1304                })?;
1305            }
1306            Rule::where_clause => where_clauses.push(build_where(inner)?),
1307            Rule::limit_clause => limit = Some(extract_limit(inner)?),
1308            _ => {}
1309        }
1310    }
1311
1312    Ok(TraverseStmt {
1313        from,
1314        via,
1315        depth,
1316        where_clauses,
1317        limit,
1318        namespace: None,
1319    })
1320}
1321
1322fn build_explain(pair: pest::iterators::Pair<'_, Rule>) -> Result<Statement, ParseError> {
1323    let mut analyze = false;
1324    let mut inner_stmt = None;
1325
1326    for child in pair.into_inner() {
1327        match child.as_rule() {
1328            Rule::analyze_flag => analyze = true,
1329            Rule::inner_stmt => {
1330                let actual = child
1331                    .into_inner()
1332                    .next()
1333                    .ok_or_else(|| ParseError::simple("EXPLAIN requires a statement"))?;
1334                inner_stmt = Some(match actual.as_rule() {
1335                    Rule::recall_events_stmt => {
1336                        build_recall_events(actual).map(Statement::RecallEvents)?
1337                    }
1338                    Rule::recall_stmt => {
1339                        build_recall(actual).map(|stmt| Statement::Recall(Box::new(stmt)))?
1340                    }
1341                    Rule::think_stmt => {
1342                        build_think(actual).map(|stmt| Statement::Think(Box::new(stmt)))?
1343                    }
1344                    Rule::forget_stmt => return unsupported_embedded_statement(Rule::forget_stmt),
1345                    Rule::correct_stmt => build_correct(actual).map(Statement::Correct)?,
1346                    Rule::supersede_stmt => build_supersede(actual).map(Statement::Supersede)?,
1347                    Rule::merge_memory_stmt => {
1348                        build_merge_memory(actual).map(Statement::MergeMemory)?
1349                    }
1350                    Rule::retract_stmt => build_retract(actual).map(Statement::Retract)?,
1351                    Rule::history_stmt => build_history(actual).map(Statement::History)?,
1352                    Rule::traverse_stmt => build_traverse(actual).map(Statement::Traverse)?,
1353                    Rule::inspect_stmt => build_inspect(actual).map(Statement::Inspect)?,
1354                    Rule::trace_stmt => build_trace(actual).map(Statement::Trace)?,
1355                    Rule::explain_causes_stmt => {
1356                        build_explain_causes(actual).map(Statement::ExplainCauses)?
1357                    }
1358                    Rule::what_if_stmt => build_what_if(actual).map(Statement::WhatIf)?,
1359                    Rule::counterfactual_stmt => {
1360                        build_counterfactual(actual).map(Statement::Counterfactual)?
1361                    }
1362                    Rule::show_policies_stmt => {
1363                        build_show_policies(actual).map(Statement::ShowPolicies)?
1364                    }
1365                    Rule::explain_policy_stmt => {
1366                        build_explain_policy(actual).map(Statement::ExplainPolicy)?
1367                    }
1368                    _ => {
1369                        return Err(ParseError::simple(format!(
1370                            "EXPLAIN not supported for {:?}",
1371                            actual.as_rule()
1372                        )));
1373                    }
1374                });
1375            }
1376            _ => {}
1377        }
1378    }
1379
1380    let inner = inner_stmt.ok_or_else(|| ParseError::simple("EXPLAIN requires a statement"))?;
1381
1382    Ok(Statement::Explain(ExplainStmt {
1383        analyze,
1384        inner: Box::new(inner),
1385    }))
1386}
1387
1388fn build_explain_causes(
1389    pair: pest::iterators::Pair<'_, Rule>,
1390) -> Result<ExplainCausesStmt, ParseError> {
1391    let mut target = String::new();
1392    let mut namespace = None;
1393    let mut depth = None;
1394
1395    for inner in pair.into_inner() {
1396        match inner.as_rule() {
1397            Rule::string_literal => target = extract_string_value(inner)?,
1398            Rule::parameter => target = inner.as_str().to_string(),
1399            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1400            Rule::causes_depth_clause => {
1401                depth = Some(extract_integer_from_clause(inner)?);
1402            }
1403            _ => {}
1404        }
1405    }
1406
1407    Ok(ExplainCausesStmt {
1408        target,
1409        namespace,
1410        depth,
1411    })
1412}
1413
1414fn build_what_if(pair: pest::iterators::Pair<'_, Rule>) -> Result<WhatIfStmt, ParseError> {
1415    let mut intervention = String::new();
1416    let mut outcome = String::new();
1417    let mut namespace = None;
1418    let mut got_first = false;
1419
1420    for inner in pair.into_inner() {
1421        match inner.as_rule() {
1422            Rule::string_literal | Rule::parameter => {
1423                let val = if inner.as_rule() == Rule::string_literal {
1424                    extract_string_value(inner)?
1425                } else {
1426                    inner.as_str().to_string()
1427                };
1428                if !got_first {
1429                    intervention = val;
1430                    got_first = true;
1431                } else {
1432                    outcome = val;
1433                }
1434            }
1435            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1436            _ => {}
1437        }
1438    }
1439
1440    Ok(WhatIfStmt {
1441        intervention,
1442        outcome,
1443        namespace,
1444    })
1445}
1446
1447fn build_counterfactual(
1448    pair: pest::iterators::Pair<'_, Rule>,
1449) -> Result<CounterfactualStmt, ParseError> {
1450    let mut antecedent = String::new();
1451    let mut consequent = String::new();
1452    let mut namespace = None;
1453    let mut got_first = false;
1454
1455    for inner in pair.into_inner() {
1456        match inner.as_rule() {
1457            Rule::string_literal | Rule::parameter => {
1458                let val = if inner.as_rule() == Rule::string_literal {
1459                    extract_string_value(inner)?
1460                } else {
1461                    inner.as_str().to_string()
1462                };
1463                if !got_first {
1464                    antecedent = val;
1465                    got_first = true;
1466                } else {
1467                    consequent = val;
1468                }
1469            }
1470            Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1471            _ => {}
1472        }
1473    }
1474
1475    Ok(CounterfactualStmt {
1476        antecedent,
1477        consequent,
1478        namespace,
1479    })
1480}
1481
1482fn build_set_assignment_list(
1483    pair: pest::iterators::Pair<'_, Rule>,
1484) -> Result<Vec<SetAssignment>, ParseError> {
1485    let mut assignments = vec![];
1486    for child in pair.into_inner() {
1487        if child.as_rule() == Rule::set_assignment {
1488            assignments.push(build_set_assignment(child)?);
1489        }
1490    }
1491    Ok(assignments)
1492}
1493
1494fn build_set_assignment(
1495    pair: pest::iterators::Pair<'_, Rule>,
1496) -> Result<SetAssignment, ParseError> {
1497    let mut field = String::new();
1498    let mut value = SetValue::Int(0);
1499
1500    for inner in pair.into_inner() {
1501        match inner.as_rule() {
1502            Rule::identifier => field = inner.as_str().to_string(),
1503            Rule::set_value => {
1504                let child = inner
1505                    .into_inner()
1506                    .next()
1507                    .ok_or_else(|| ParseError::simple("empty set value"))?;
1508                value = match child.as_rule() {
1509                    Rule::set_function => build_set_function(child)?,
1510                    Rule::float_literal => {
1511                        let text = child.as_str();
1512                        SetValue::Float(text.parse().map_err(|_| {
1513                            ParseError::simple(format!("invalid float in SET: '{text}'"))
1514                        })?)
1515                    }
1516                    Rule::integer_literal => {
1517                        let text = child.as_str();
1518                        SetValue::Int(text.parse().map_err(|_| {
1519                            ParseError::simple(format!("invalid integer in SET: '{text}'"))
1520                        })?)
1521                    }
1522                    Rule::string_literal => SetValue::String(extract_string_value(child)?),
1523                    _ => SetValue::Int(0),
1524                };
1525            }
1526            _ => {}
1527        }
1528    }
1529
1530    Ok(SetAssignment { field, value })
1531}
1532
1533fn build_set_function(pair: pest::iterators::Pair<'_, Rule>) -> Result<SetValue, ParseError> {
1534    let raw = pair.as_str();
1535    let is_max = raw.len() >= 3 && raw[..3].eq_ignore_ascii_case("max");
1536    let mut field = String::new();
1537    let mut val = 0.0;
1538
1539    for inner in pair.into_inner() {
1540        match inner.as_rule() {
1541            Rule::identifier => field = inner.as_str().to_string(),
1542            Rule::float_literal => {
1543                let t = inner.as_str();
1544                val = t.parse().map_err(|_| {
1545                    ParseError::simple(format!("invalid float in SET function: '{t}'"))
1546                })?;
1547            }
1548            Rule::integer_literal => {
1549                let t = inner.as_str();
1550                val = t.parse().map_err(|_| {
1551                    ParseError::simple(format!("invalid integer in SET function: '{t}'"))
1552                })?;
1553            }
1554            _ => {}
1555        }
1556    }
1557
1558    Ok(if is_max {
1559        SetValue::Max(field, val)
1560    } else {
1561        SetValue::Min(field, val)
1562    })
1563}
1564
1565// ── Multi-modal helpers ────────────────────────────────────────────────
1566
1567fn build_modality_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
1568    build_named_list(
1569        pair,
1570        Rule::modality_list,
1571        Rule::modality_name,
1572        "MODALITY clause missing modality list",
1573    )
1574}
1575
1576fn build_evidence_role_list(
1577    pair: pest::iterators::Pair<'_, Rule>,
1578) -> Result<Vec<String>, ParseError> {
1579    build_named_list(
1580        pair,
1581        Rule::evidence_role_list,
1582        Rule::evidence_role_name,
1583        "RESOURCE_ROLE clause missing evidence role list",
1584    )
1585}
1586
1587fn build_hydration_mode_list(
1588    pair: pest::iterators::Pair<'_, Rule>,
1589) -> Result<Vec<String>, ParseError> {
1590    build_named_list(
1591        pair,
1592        Rule::hydration_mode_list,
1593        Rule::hydration_mode_name,
1594        "HYDRATION clause missing hydration mode list",
1595    )
1596}
1597
1598fn build_artifact_kind_list(
1599    pair: pest::iterators::Pair<'_, Rule>,
1600) -> Result<Vec<String>, ParseError> {
1601    build_named_list(
1602        pair,
1603        Rule::artifact_kind_list,
1604        Rule::artifact_kind_name,
1605        "ARTIFACT clause missing artifact kind list",
1606    )
1607}
1608
1609fn build_named_list(
1610    pair: pest::iterators::Pair<'_, Rule>,
1611    list_rule: Rule,
1612    item_rule: Rule,
1613    missing_message: &str,
1614) -> Result<Vec<String>, ParseError> {
1615    let list = pair
1616        .into_inner()
1617        .find(|p| p.as_rule() == list_rule)
1618        .ok_or_else(|| ParseError::simple(missing_message))?;
1619    Ok(list
1620        .into_inner()
1621        .filter(|p| p.as_rule() == item_rule)
1622        .map(|p| p.as_str().to_lowercase())
1623        .collect())
1624}
1625
1626// ── CREATE REALM / DROP REALM ──────────────────
1627
1628fn build_create_realm(
1629    pair: pest::iterators::Pair<'_, Rule>,
1630) -> Result<CreateRealmStmt, ParseError> {
1631    let mut name = String::new();
1632    let mut description = None;
1633
1634    for inner in pair.into_inner() {
1635        match inner.as_rule() {
1636            Rule::string_literal if name.is_empty() => {
1637                name = extract_string_value(inner)?;
1638            }
1639            Rule::realm_description => {
1640                for child in inner.into_inner() {
1641                    if child.as_rule() == Rule::string_literal {
1642                        description = Some(extract_string_value(child)?);
1643                    }
1644                }
1645            }
1646            _ => {}
1647        }
1648    }
1649
1650    Ok(CreateRealmStmt { name, description })
1651}
1652
1653fn build_drop_realm(pair: pest::iterators::Pair<'_, Rule>) -> Result<DropRealmStmt, ParseError> {
1654    let mut name = String::new();
1655    let mut confirm = false;
1656
1657    for inner in pair.into_inner() {
1658        match inner.as_rule() {
1659            Rule::string_literal if name.is_empty() => {
1660                name = extract_string_value(inner)?;
1661            }
1662            Rule::confirm_flag => confirm = true,
1663            _ => {}
1664        }
1665    }
1666
1667    Ok(DropRealmStmt { name, confirm })
1668}
1669
1670// ── GRANT / REVOKE ─────────────────────────────
1671
1672fn build_action_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1673    pair.into_inner()
1674        .filter(|p| p.as_rule() == Rule::action_name)
1675        .map(|p| p.as_str().to_lowercase())
1676        .collect()
1677}
1678
1679fn build_grant_target(pair: pest::iterators::Pair<'_, Rule>) -> Result<GrantTarget, ParseError> {
1680    let raw = pair.as_str();
1681    let is_namespace = raw.to_ascii_lowercase().contains("namespace");
1682    let string_val = pair
1683        .into_inner()
1684        .find(|p| p.as_rule() == Rule::string_literal)
1685        .map(extract_string_value)
1686        .transpose()?
1687        .unwrap_or_default();
1688
1689    Ok(if is_namespace {
1690        GrantTarget::Namespace(string_val)
1691    } else {
1692        GrantTarget::Realm(string_val)
1693    })
1694}
1695
1696fn build_principal_ref(pair: pest::iterators::Pair<'_, Rule>) -> Result<PrincipalRef, ParseError> {
1697    let raw = pair.as_str();
1698    let is_team = raw.to_ascii_lowercase().contains("team");
1699    let string_val = pair
1700        .into_inner()
1701        .find(|p| p.as_rule() == Rule::string_literal)
1702        .map(extract_string_value)
1703        .transpose()?
1704        .unwrap_or_default();
1705
1706    Ok(if is_team {
1707        PrincipalRef::Team(string_val)
1708    } else {
1709        PrincipalRef::Agent(string_val)
1710    })
1711}
1712
1713fn build_grant(pair: pest::iterators::Pair<'_, Rule>) -> Result<GrantStmt, ParseError> {
1714    let mut actions = Vec::new();
1715    let mut target = None;
1716    let mut principal = None;
1717
1718    for inner in pair.into_inner() {
1719        match inner.as_rule() {
1720            Rule::action_list => actions = build_action_list(inner),
1721            Rule::grant_target => target = Some(build_grant_target(inner)?),
1722            Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1723            _ => {}
1724        }
1725    }
1726
1727    Ok(GrantStmt {
1728        actions,
1729        target: target
1730            .ok_or_else(|| ParseError::simple("GRANT requires ON NAMESPACE/REALM clause"))?,
1731        principal: principal
1732            .ok_or_else(|| ParseError::simple("GRANT requires TO AGENT/TEAM clause"))?,
1733    })
1734}
1735
1736fn build_revoke(pair: pest::iterators::Pair<'_, Rule>) -> Result<RevokeStmt, ParseError> {
1737    let mut actions = Vec::new();
1738    let mut target = None;
1739    let mut principal = None;
1740
1741    for inner in pair.into_inner() {
1742        match inner.as_rule() {
1743            Rule::action_list => actions = build_action_list(inner),
1744            Rule::grant_target => target = Some(build_grant_target(inner)?),
1745            Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1746            _ => {}
1747        }
1748    }
1749
1750    Ok(RevokeStmt {
1751        actions,
1752        target: target
1753            .ok_or_else(|| ParseError::simple("REVOKE requires ON NAMESPACE/REALM clause"))?,
1754        principal: principal
1755            .ok_or_else(|| ParseError::simple("REVOKE requires FROM AGENT/TEAM clause"))?,
1756    })
1757}
1758
1759// ── SHOW POLICIES / EXPLAIN POLICY ────────────
1760
1761fn build_show_policies(
1762    pair: pest::iterators::Pair<'_, Rule>,
1763) -> Result<ShowPoliciesStmt, ParseError> {
1764    let mut principal = None;
1765
1766    for inner in pair.into_inner() {
1767        if inner.as_rule() == Rule::principal_ref {
1768            principal = Some(build_principal_ref(inner)?);
1769        }
1770    }
1771
1772    Ok(ShowPoliciesStmt { principal })
1773}
1774
1775fn build_explain_policy(
1776    pair: pest::iterators::Pair<'_, Rule>,
1777) -> Result<ExplainPolicyStmt, ParseError> {
1778    let mut principal = None;
1779    let mut resource_type = String::new();
1780    let mut resource_name = String::new();
1781    let mut action = String::new();
1782
1783    let raw = pair.as_str();
1784    // Detect resource type from the raw text (ON NAMESPACE vs ON REALM).
1785    let raw_lower = raw.to_ascii_lowercase();
1786    if raw_lower.contains("namespace") {
1787        resource_type = "namespace".to_string();
1788    } else if raw_lower.contains("realm") {
1789        resource_type = "realm".to_string();
1790    }
1791
1792    for inner in pair.into_inner() {
1793        match inner.as_rule() {
1794            Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1795            Rule::string_literal if resource_name.is_empty() => {
1796                resource_name = extract_string_value(inner)?;
1797            }
1798            Rule::action_name => action = inner.as_str().to_lowercase(),
1799            _ => {}
1800        }
1801    }
1802
1803    Ok(ExplainPolicyStmt {
1804        principal: principal
1805            .ok_or_else(|| ParseError::simple("EXPLAIN POLICY requires FOR AGENT/TEAM clause"))?,
1806        resource_type,
1807        resource_name,
1808        action,
1809    })
1810}
1811
1812// ── SET TIER_POLICY ────────────────────────────
1813
1814fn build_set_tier_policy(
1815    pair: pest::iterators::Pair<'_, Rule>,
1816) -> Result<SetTierPolicyStmt, ParseError> {
1817    let mut field = String::new();
1818    let mut value = None;
1819
1820    for inner in pair.into_inner() {
1821        match inner.as_rule() {
1822            Rule::tier_policy_field => {
1823                field = inner.as_str().to_lowercase();
1824            }
1825            Rule::tier_policy_value => {
1826                let val_inner = inner
1827                    .into_inner()
1828                    .next()
1829                    .ok_or_else(|| ParseError::simple("missing tier policy value"))?;
1830                value = Some(match val_inner.as_rule() {
1831                    Rule::string_literal => TierPolicyValue::Str(extract_string_value(val_inner)?),
1832                    Rule::float_literal => {
1833                        let v: f64 = val_inner
1834                            .as_str()
1835                            .parse()
1836                            .map_err(|_| ParseError::simple("invalid float in SET TIER_POLICY"))?;
1837                        TierPolicyValue::Float(v)
1838                    }
1839                    Rule::integer_literal => {
1840                        let v: i64 = val_inner.as_str().parse().map_err(|_| {
1841                            ParseError::simple("invalid integer in SET TIER_POLICY")
1842                        })?;
1843                        TierPolicyValue::Int(v)
1844                    }
1845                    _ => {
1846                        return Err(ParseError::simple("unexpected tier policy value type"));
1847                    }
1848                });
1849            }
1850            _ => {}
1851        }
1852    }
1853
1854    Ok(SetTierPolicyStmt {
1855        field,
1856        value: value.ok_or_else(|| ParseError::simple("missing value in SET TIER_POLICY"))?,
1857    })
1858}
1859
1860// ── Tests ──────────────────────────────────────────────────────────────
1861
1862#[cfg(test)]
1863mod tests {
1864    use super::*;
1865
1866    #[test]
1867    fn parse_minimal_recall() {
1868        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
1869        match stmt {
1870            Statement::Recall(r) => {
1871                assert_eq!(r.layers, vec![Layer::Episodic]);
1872                assert_eq!(r.about, "test");
1873                assert!(r.limit.is_none());
1874            }
1875            other => panic!("expected Recall, got {other:?}"),
1876        }
1877    }
1878
1879    #[test]
1880    fn parse_full_recall() {
1881        let q = r#"
1882            RECALL semantic, episodic
1883              ABOUT "vector database optimization"
1884              INVOLVING "HNSW", "benchmark"
1885              AFTER "2026-03-01"
1886              EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading
1887              FOLLOW CAUSES DEPTH 3
1888              WHERE importance > 0.4
1889              WHERE confidence > 0.8
1890              AS NARRATIVE
1891              BUDGET 4096
1892              NAMESPACE shared_knowledge
1893              CONSISTENCY linearizable
1894              LIMIT 20
1895        "#;
1896        let stmt = parse(q).unwrap();
1897        match stmt {
1898            Statement::Recall(r) => {
1899                assert_eq!(r.layers, vec![Layer::Semantic, Layer::Episodic]);
1900                assert_eq!(r.about, "vector database optimization");
1901                assert_eq!(r.involving.unwrap(), vec!["HNSW", "benchmark"]);
1902                assert_eq!(r.temporal, Some(TemporalClause::After("2026-03-01".into())));
1903
1904                let ex = r.expand.unwrap();
1905                assert_eq!(ex.depth, 2);
1906                assert_eq!(ex.min_weight, Some(0.3));
1907                assert_eq!(ex.activation, Some(ActivationModeAst::Spreading));
1908
1909                assert_eq!(r.follow_causes, Some(3));
1910                assert_eq!(r.where_clauses.len(), 2);
1911                assert_eq!(r.output_format, Some(OutputFormat::Narrative));
1912                assert_eq!(r.budget, Some(4096));
1913                assert_eq!(r.namespace, Some("shared_knowledge".into()));
1914                assert_eq!(r.consistency, Some(ConsistencyLevel::Linearizable));
1915                assert_eq!(r.limit, Some(20));
1916            }
1917            other => panic!("expected Recall, got {other:?}"),
1918        }
1919    }
1920
1921    #[test]
1922    fn parse_think_with_budget() {
1923        let q = r#"THINK ABOUT "How should I optimize HNSW?" BUDGET 4096"#;
1924        let stmt = parse(q).unwrap();
1925        match stmt {
1926            Statement::Think(t) => {
1927                assert!(t.about.contains("HNSW"));
1928                assert_eq!(t.budget, Some(4096));
1929                assert_eq!(t.mode, RetrievalMode::Local);
1930                assert_eq!(t.community_depth, None);
1931            }
1932            other => panic!("expected Think, got {other:?}"),
1933        }
1934    }
1935
1936    #[test]
1937    fn parse_think_global() {
1938        let q = r#"THINK GLOBAL ABOUT "summarize themes""#;
1939        let stmt = parse(q).unwrap();
1940        match stmt {
1941            Statement::Think(t) => {
1942                assert!(t.about.contains("themes"));
1943                assert_eq!(t.mode, RetrievalMode::Global);
1944                assert_eq!(t.community_depth, None);
1945            }
1946            other => panic!("expected Think, got {other:?}"),
1947        }
1948    }
1949
1950    #[test]
1951    fn parse_think_hybrid_with_community_depth() {
1952        let q = r#"THINK ABOUT "cross-domain links" MODE hybrid COMMUNITY_DEPTH 3"#;
1953        let stmt = parse(q).unwrap();
1954        match stmt {
1955            Statement::Think(t) => {
1956                assert!(t.about.contains("cross-domain"));
1957                assert_eq!(t.mode, RetrievalMode::Hybrid);
1958                assert_eq!(t.community_depth, Some(3));
1959                assert!(!t.hybrid);
1960            }
1961            other => panic!("expected Think, got {other:?}"),
1962        }
1963    }
1964
1965    #[test]
1966    fn parse_think_query_text_hybrid_clause() {
1967        let q = r#"THINK ABOUT "cross-domain links" HYBRID"#;
1968        let stmt = parse(q).unwrap();
1969        match stmt {
1970            Statement::Think(t) => {
1971                assert_eq!(t.mode, RetrievalMode::Local);
1972                assert!(t.hybrid);
1973            }
1974            other => panic!("expected Think, got {other:?}"),
1975        }
1976    }
1977
1978    #[test]
1979    fn parse_think_mode_local_explicit() {
1980        let q = r#"THINK ABOUT "x" MODE local"#;
1981        let stmt = parse(q).unwrap();
1982        match stmt {
1983            Statement::Think(t) => {
1984                assert_eq!(t.mode, RetrievalMode::Local);
1985            }
1986            other => panic!("expected Think, got {other:?}"),
1987        }
1988    }
1989
1990    #[test]
1991    fn roundtrip_think_global() {
1992        let q = r#"THINK GLOBAL ABOUT "themes" BUDGET 2048 COMMUNITY_DEPTH 2"#;
1993        let stmt1 = parse(q).unwrap();
1994        let rendered = stmt1.to_string();
1995        let stmt2 = parse(&rendered).unwrap();
1996        assert_eq!(stmt1, stmt2);
1997    }
1998
1999    #[test]
2000    fn roundtrip_think_hybrid() {
2001        let q = r#"THINK ABOUT "links" MODE hybrid COMMUNITY_DEPTH 5"#;
2002        let stmt1 = parse(q).unwrap();
2003        let rendered = stmt1.to_string();
2004        let stmt2 = parse(&rendered).unwrap();
2005        assert_eq!(stmt1, stmt2);
2006    }
2007
2008    #[test]
2009    fn roundtrip_think_query_text_hybrid() {
2010        let q = r#"THINK ABOUT "links" HYBRID"#;
2011        let stmt1 = parse(q).unwrap();
2012        let rendered = stmt1.to_string();
2013        let stmt2 = parse(&rendered).unwrap();
2014        assert_eq!(stmt1, stmt2);
2015    }
2016
2017    #[test]
2018    fn parse_think_mode_raptor() {
2019        let q = r#"THINK ABOUT "architecture overview" MODE raptor"#;
2020        let stmt = parse(q).unwrap();
2021        match stmt {
2022            Statement::Think(t) => {
2023                assert!(t.about.contains("architecture"));
2024                assert_eq!(t.mode, RetrievalMode::Raptor);
2025            }
2026            other => panic!("expected Think, got {other:?}"),
2027        }
2028    }
2029
2030    #[test]
2031    fn parse_think_mode_adaptive() {
2032        let q = r#"THINK ABOUT "deployment strategies" MODE adaptive"#;
2033        let stmt = parse(q).unwrap();
2034        match stmt {
2035            Statement::Think(t) => {
2036                assert!(t.about.contains("deployment"));
2037                assert_eq!(t.mode, RetrievalMode::Adaptive);
2038            }
2039            other => panic!("expected Think, got {other:?}"),
2040        }
2041    }
2042
2043    #[test]
2044    fn roundtrip_think_raptor() {
2045        let q = r#"THINK ABOUT "overview" MODE raptor COMMUNITY_DEPTH 3"#;
2046        let stmt1 = parse(q).unwrap();
2047        let rendered = stmt1.to_string();
2048        let stmt2 = parse(&rendered).unwrap();
2049        assert_eq!(stmt1, stmt2);
2050    }
2051
2052    #[test]
2053    fn roundtrip_think_adaptive() {
2054        let q = r#"THINK ABOUT "analysis" MODE adaptive"#;
2055        let stmt1 = parse(q).unwrap();
2056        let rendered = stmt1.to_string();
2057        let stmt2 = parse(&rendered).unwrap();
2058        assert_eq!(stmt1, stmt2);
2059    }
2060
2061    #[test]
2062    fn parse_correct() {
2063        let q = r#"CORRECT "some_id" SET description = "updated", confidence = 0.9 REASON "fix" OBSERVED AT "2026-01-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2064        let stmt = parse(q).unwrap();
2065        match stmt {
2066            Statement::Correct(c) => {
2067                assert_eq!(c.target, SemanticTargetRef::Memory("some_id".into()));
2068                assert_eq!(c.updates.len(), 2);
2069                assert_eq!(c.reason.as_deref(), Some("fix"));
2070                assert_eq!(c.observed_at.as_deref(), Some("2026-01-01T00:00:00Z"));
2071                assert_eq!(c.caused_by.as_deref(), Some("cause_id"));
2072                assert_eq!(c.namespace.as_deref(), Some("custom"));
2073            }
2074            other => panic!("expected Correct, got {other:?}"),
2075        }
2076    }
2077
2078    #[test]
2079    fn parse_supersede() {
2080        let q = r#"SUPERSEDE LOGICAL "some_id" SET description = "replacement", confidence = 0.8 REASON "new authority" OBSERVED AT "2026-02-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2081        let stmt = parse(q).unwrap();
2082        match stmt {
2083            Statement::Supersede(s) => {
2084                assert_eq!(s.target, SemanticTargetRef::Logical("some_id".into()));
2085                assert_eq!(s.updates.len(), 2);
2086                assert_eq!(s.reason.as_deref(), Some("new authority"));
2087                assert_eq!(s.observed_at.as_deref(), Some("2026-02-01T00:00:00Z"));
2088                assert_eq!(s.caused_by.as_deref(), Some("cause_id"));
2089                assert_eq!(s.namespace.as_deref(), Some("custom"));
2090            }
2091            other => panic!("expected Supersede, got {other:?}"),
2092        }
2093    }
2094
2095    #[test]
2096    fn parse_merge_memory() {
2097        let q = r#"MERGE MEMORY "source_a", REVISION "source_b" INTO LOGICAL "target_id" SET description = "canonical", confidence = 0.95 REASON "deduplicate" OBSERVED AT "2026-03-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2098        let stmt = parse(q).unwrap();
2099        match stmt {
2100            Statement::MergeMemory(m) => {
2101                assert_eq!(
2102                    m.sources,
2103                    vec![
2104                        SemanticTargetRef::Memory("source_a".into()),
2105                        SemanticTargetRef::Revision("source_b".into()),
2106                    ]
2107                );
2108                assert_eq!(m.target, SemanticTargetRef::Logical("target_id".into()));
2109                assert_eq!(m.updates.len(), 2);
2110                assert_eq!(m.reason.as_deref(), Some("deduplicate"));
2111                assert_eq!(m.observed_at.as_deref(), Some("2026-03-01T00:00:00Z"));
2112                assert_eq!(m.caused_by.as_deref(), Some("cause_id"));
2113                assert_eq!(m.namespace.as_deref(), Some("custom"));
2114            }
2115            other => panic!("expected MergeMemory, got {other:?}"),
2116        }
2117    }
2118
2119    #[test]
2120    fn parse_retract() {
2121        let q = r#"RETRACT REVISION "some_id" REASON "obsolete" OBSERVED AT "2026-01-01" CAUSED BY "cause_id" NAMESPACE custom"#;
2122        let stmt = parse(q).unwrap();
2123        match stmt {
2124            Statement::Retract(r) => {
2125                assert_eq!(r.target, SemanticTargetRef::Revision("some_id".into()));
2126                assert_eq!(r.reason.as_deref(), Some("obsolete"));
2127                assert_eq!(r.observed_at.as_deref(), Some("2026-01-01"));
2128                assert_eq!(r.caused_by.as_deref(), Some("cause_id"));
2129                assert_eq!(r.namespace.as_deref(), Some("custom"));
2130            }
2131            other => panic!("expected Retract, got {other:?}"),
2132        }
2133    }
2134
2135    #[test]
2136    fn parse_remember_is_unsupported() {
2137        let err = parse(r#"REMEMBER episode CONTENT "event happened""#).unwrap_err();
2138        assert!(err.message.contains("REMEMBER is not supported"));
2139    }
2140
2141    #[test]
2142    fn parse_forget_is_unsupported() {
2143        let err = parse(r#"FORGET "01J000000000000000000000""#).unwrap_err();
2144        assert!(err.message.contains("FORGET is not supported"));
2145    }
2146
2147    #[test]
2148    fn parse_connect_is_unsupported() {
2149        let q = r#"CONNECT "HNSW_indexing" TO "approximate_nearest_neighbors" AS related_to WEIGHT 0.9"#;
2150        let err = parse(q).unwrap_err();
2151        assert!(err.message.contains("CONNECT is not supported"));
2152    }
2153
2154    #[test]
2155    fn parse_consolidate_is_unsupported() {
2156        let err = parse("CONSOLIDATE WHERE episodic.access_count > 5").unwrap_err();
2157        assert!(err.message.contains("CONSOLIDATE is not supported"));
2158    }
2159
2160    #[test]
2161    fn parse_watch_is_unsupported() {
2162        let err = parse("WATCH ALL FORMAT json").unwrap_err();
2163        assert!(err.message.contains("WATCH is not supported"));
2164    }
2165
2166    #[test]
2167    fn parse_explain_analyze_forget_is_unsupported() {
2168        let err = parse(r#"EXPLAIN ANALYZE FORGET "01J000000000000000000000""#).unwrap_err();
2169        assert!(err.message.contains("FORGET is not supported"));
2170    }
2171
2172    #[test]
2173    fn parse_inspect() {
2174        let q = r#"INSPECT LOGICAL "record_id""#;
2175        let stmt = parse(q).unwrap();
2176        match stmt {
2177            Statement::Inspect(i) => {
2178                assert_eq!(i.target, SemanticTargetRef::Logical("record_id".into()));
2179            }
2180            other => panic!("expected Inspect, got {other:?}"),
2181        }
2182    }
2183
2184    #[test]
2185    fn parse_history() {
2186        let q = r#"HISTORY REVISION "record_id" NAMESPACE custom"#;
2187        let stmt = parse(q).unwrap();
2188        match stmt {
2189            Statement::History(h) => {
2190                assert_eq!(h.target, SemanticTargetRef::Revision("record_id".into()));
2191                assert_eq!(h.namespace.as_deref(), Some("custom"));
2192            }
2193            other => panic!("expected History, got {other:?}"),
2194        }
2195    }
2196
2197    #[test]
2198    fn parse_trace() {
2199        let q = r#"TRACE LOGICAL "semantic:caching_best_practices""#;
2200        let stmt = parse(q).unwrap();
2201        match stmt {
2202            Statement::Trace(t) => {
2203                assert_eq!(
2204                    t.target,
2205                    SemanticTargetRef::Logical("semantic:caching_best_practices".into())
2206                );
2207            }
2208            other => panic!("expected Trace, got {other:?}"),
2209        }
2210    }
2211
2212    #[test]
2213    fn parse_error_unknown_verb() {
2214        let err = parse("SELECT * FROM memories").unwrap_err();
2215        assert!(err.message.contains("unknown verb"));
2216        assert!(err.message.contains("RECALL"));
2217    }
2218
2219    #[test]
2220    fn parse_error_unterminated_string() {
2221        let err = parse(r#"RECALL episodic ABOUT "unterminated"#).unwrap_err();
2222        assert!(err.line >= 1);
2223        assert!(err.column >= 1);
2224    }
2225
2226    #[test]
2227    fn parse_case_insensitive() {
2228        let q1 = parse(r#"recall episodic about "test""#).unwrap();
2229        let q2 = parse(r#"RECALL EPISODIC ABOUT "test""#).unwrap();
2230        let q3 = parse(r#"Recall Episodic About "test""#).unwrap();
2231        assert_eq!(q1, q2);
2232        assert_eq!(q2, q3);
2233    }
2234
2235    #[test]
2236    fn parse_with_comments() {
2237        let q = "-- this is a comment\nRECALL episodic ABOUT \"test\"";
2238        let stmt = parse(q).unwrap();
2239        assert!(matches!(stmt, Statement::Recall(_)));
2240    }
2241
2242    #[test]
2243    fn parse_single_quoted_strings() {
2244        let q = "RECALL episodic ABOUT 'test query'";
2245        let stmt = parse(q).unwrap();
2246        match stmt {
2247            Statement::Recall(r) => assert_eq!(r.about, "test query"),
2248            other => panic!("expected Recall, got {other:?}"),
2249        }
2250    }
2251
2252    #[test]
2253    fn parse_multiline_query() {
2254        let q = "RECALL episodic\n  ABOUT \"test\"\n  LIMIT 10";
2255        let stmt = parse(q).unwrap();
2256        match stmt {
2257            Statement::Recall(r) => {
2258                assert_eq!(r.about, "test");
2259                assert_eq!(r.limit, Some(10));
2260            }
2261            other => panic!("expected Recall, got {other:?}"),
2262        }
2263    }
2264
2265    #[test]
2266    fn parse_empty_input() {
2267        let err = parse("").unwrap_err();
2268        assert!(err.line >= 1);
2269    }
2270
2271    #[test]
2272    fn parse_between_temporal() {
2273        let q = r#"RECALL episodic ABOUT "test" BETWEEN "2026-03-01" AND "2026-03-15""#;
2274        let stmt = parse(q).unwrap();
2275        match stmt {
2276            Statement::Recall(r) => {
2277                assert_eq!(
2278                    r.temporal,
2279                    Some(TemporalClause::Between {
2280                        start: "2026-03-01".into(),
2281                        end: "2026-03-15".into(),
2282                    })
2283                );
2284            }
2285            other => panic!("expected Recall, got {other:?}"),
2286        }
2287    }
2288
2289    #[test]
2290    fn parse_concept_9_4_semantic_search() {
2291        let q = r#"
2292            RECALL semantic, episodic
2293              ABOUT "vector database optimization"
2294              EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading
2295              WHERE importance > 0.4
2296              LIMIT 20
2297        "#;
2298        let stmt = parse(q).unwrap();
2299        assert!(matches!(stmt, Statement::Recall(_)));
2300    }
2301
2302    #[test]
2303    fn parse_concept_9_4_temporal_narrative() {
2304        // This concept query has INVOLVING but no ABOUT, which the grammar requires.
2305        // For now, test with ABOUT added.
2306        let q = r#"
2307            RECALL episodic
2308              ABOUT "deployment and production events"
2309              INVOLVING "deployment", "production"
2310              BETWEEN "2026-03-01" AND "2026-03-15"
2311              AS NARRATIVE
2312        "#;
2313        let stmt = parse(q).unwrap();
2314        assert!(matches!(stmt, Statement::Recall(_)));
2315    }
2316
2317    #[test]
2318    fn parse_concept_9_4_causal_chain() {
2319        let q = r#"
2320            RECALL episodic
2321              ABOUT "production outage"
2322              FOLLOW CAUSES DEPTH 3
2323              AS CAUSAL_CHAIN
2324        "#;
2325        let stmt = parse(q).unwrap();
2326        match stmt {
2327            Statement::Recall(r) => {
2328                assert_eq!(r.follow_causes, Some(3));
2329                assert_eq!(r.output_format, Some(OutputFormat::CausalChain));
2330            }
2331            other => panic!("expected Recall, got {other:?}"),
2332        }
2333    }
2334
2335    #[test]
2336    fn parse_concept_9_4_think() {
2337        let q = r#"
2338            THINK
2339              ABOUT "How should I optimize HNSW for high-dimensional embeddings?"
2340              EXPAND GRAPH DEPTH 2 ACTIVATION spreading
2341              BUDGET 4096
2342        "#;
2343        let stmt = parse(q).unwrap();
2344        match stmt {
2345            Statement::Think(t) => {
2346                assert_eq!(t.budget, Some(4096));
2347                assert!(t.expand.is_some());
2348            }
2349            other => panic!("expected Think, got {other:?}"),
2350        }
2351    }
2352
2353    #[test]
2354    fn parse_concept_9_4_connect_is_unsupported() {
2355        let q = r#"
2356            CONNECT "HNSW_indexing" TO "approximate_nearest_neighbors"
2357              AS related_to
2358              WEIGHT 0.9
2359        "#;
2360        let err = parse(q).unwrap_err();
2361        assert!(err.message.contains("CONNECT is not supported"));
2362    }
2363
2364    #[test]
2365    fn parse_concept_9_4_trace() {
2366        let q = r#"TRACE "semantic:caching_best_practices""#;
2367        let stmt = parse(q).unwrap();
2368        assert!(matches!(stmt, Statement::Trace(_)));
2369    }
2370
2371    #[test]
2372    fn parse_concept_9_4_cross_agent() {
2373        let q = r#"
2374            RECALL semantic
2375              ABOUT "API rate limiting patterns"
2376              WHERE confidence > 0.8
2377              NAMESPACE shared_knowledge
2378        "#;
2379        let stmt = parse(q).unwrap();
2380        match stmt {
2381            Statement::Recall(r) => {
2382                assert_eq!(r.namespace, Some("shared_knowledge".into()));
2383            }
2384            other => panic!("expected Recall, got {other:?}"),
2385        }
2386    }
2387
2388    #[test]
2389    fn parse_concept_9_4_consistency() {
2390        let q = r#"
2391            RECALL semantic
2392              ABOUT "compliance rules"
2393              CONSISTENCY linearizable
2394        "#;
2395        let stmt = parse(q).unwrap();
2396        match stmt {
2397            Statement::Recall(r) => {
2398                assert_eq!(r.consistency, Some(ConsistencyLevel::Linearizable));
2399            }
2400            other => panic!("expected Recall, got {other:?}"),
2401        }
2402    }
2403
2404    #[test]
2405    fn roundtrip_recall() {
2406        let q = r#"RECALL episodic ABOUT "test" LIMIT 10"#;
2407        let stmt1 = parse(q).unwrap();
2408        let rendered = stmt1.to_string();
2409        let stmt2 = parse(&rendered).unwrap();
2410        assert_eq!(stmt1, stmt2);
2411    }
2412
2413    #[test]
2414    fn roundtrip_think() {
2415        let q = r#"THINK ABOUT "optimize queries" BUDGET 4096 LIMIT 5"#;
2416        let stmt1 = parse(q).unwrap();
2417        let rendered = stmt1.to_string();
2418        let stmt2 = parse(&rendered).unwrap();
2419        assert_eq!(stmt1, stmt2);
2420    }
2421
2422    #[test]
2423    fn roundtrip_history() {
2424        let q = r#"HISTORY "id" NAMESPACE custom"#;
2425        let stmt1 = parse(q).unwrap();
2426        let rendered = stmt1.to_string();
2427        let stmt2 = parse(&rendered).unwrap();
2428        assert_eq!(stmt1, stmt2);
2429    }
2430
2431    #[test]
2432    fn roundtrip_inspect() {
2433        let q = r#"INSPECT "id""#;
2434        let stmt1 = parse(q).unwrap();
2435        let rendered = stmt1.to_string();
2436        let stmt2 = parse(&rendered).unwrap();
2437        assert_eq!(stmt1, stmt2);
2438    }
2439
2440    #[test]
2441    fn roundtrip_trace() {
2442        let q = r#"TRACE "id""#;
2443        let stmt1 = parse(q).unwrap();
2444        let rendered = stmt1.to_string();
2445        let stmt2 = parse(&rendered).unwrap();
2446        assert_eq!(stmt1, stmt2);
2447    }
2448
2449    #[test]
2450    fn roundtrip_full_recall() {
2451        let q = r#"RECALL semantic, episodic ABOUT "optimization" INVOLVING "HNSW" AFTER "2026-03-01" EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading WHERE importance > 0.4 BUDGET 4096 NAMESPACE test LIMIT 20"#;
2452        let stmt1 = parse(q).unwrap();
2453        let rendered = stmt1.to_string();
2454        let stmt2 = parse(&rendered).unwrap();
2455        assert_eq!(stmt1, stmt2);
2456    }
2457
2458    #[test]
2459    fn fuzz_no_panics() {
2460        // Quick fuzz — random-ish strings should never panic, only return Err.
2461        let inputs = [
2462            "",
2463            "   ",
2464            "\n\n",
2465            "SELECT * FROM x",
2466            "RECALL",
2467            "RECALL episodic",
2468            "RECALL episodic ABOUT",
2469            "RECALL ABOUT \"x\"",
2470            "THINK",
2471            "REMEMBER",
2472            "FORGET",
2473            "CONNECT",
2474            "INSPECT",
2475            "HISTORY",
2476            "TRACE",
2477            "CONSOLIDATE",
2478            "😀 unicode",
2479            "RECALL episodic ABOUT \"x\" LIMIT -1",
2480            "RECALL episodic ABOUT \"x\" LIMIT 999999999999",
2481        ];
2482        let long_input = "A".repeat(10000);
2483        let mut inputs_vec: Vec<&str> = inputs.to_vec();
2484        inputs_vec.push(long_input.as_str());
2485        for input in inputs_vec {
2486            let _ = parse(input); // must not panic
2487        }
2488    }
2489
2490    // ── Aggregations & Projections ─────────────────────────
2491
2492    #[test]
2493    fn parse_group_by_count() {
2494        let stmt = parse(r#"RECALL episodic ABOUT "test" GROUP BY entity_type COUNT"#).unwrap();
2495        match stmt {
2496            Statement::Recall(r) => {
2497                let gb = r.group_by.unwrap();
2498                assert_eq!(gb.field, "entity_type");
2499                assert_eq!(gb.function, AggFunction::Count);
2500            }
2501            _ => panic!("expected Recall"),
2502        }
2503    }
2504
2505    #[test]
2506    fn parse_group_by_avg() {
2507        let stmt = parse(r#"RECALL episodic ABOUT "test" GROUP BY importance AVG"#).unwrap();
2508        match stmt {
2509            Statement::Recall(r) => {
2510                let gb = r.group_by.unwrap();
2511                assert_eq!(gb.field, "importance");
2512                assert_eq!(gb.function, AggFunction::Avg);
2513            }
2514            _ => panic!("expected Recall"),
2515        }
2516    }
2517
2518    #[test]
2519    fn parse_select_projection() {
2520        let stmt = parse(r#"RECALL episodic ABOUT "test" SELECT id, summary, importance"#).unwrap();
2521        match stmt {
2522            Statement::Recall(r) => {
2523                let proj = r.projection.unwrap();
2524                assert_eq!(proj, vec!["id", "summary", "importance"]);
2525            }
2526            _ => panic!("expected Recall"),
2527        }
2528    }
2529
2530    #[test]
2531    fn parse_format_json() {
2532        let stmt = parse(r#"RECALL episodic ABOUT "test" FORMAT json"#).unwrap();
2533        match stmt {
2534            Statement::Recall(r) => {
2535                assert_eq!(r.result_format.unwrap(), OutputFormat::Json);
2536            }
2537            _ => panic!("expected Recall"),
2538        }
2539    }
2540
2541    #[test]
2542    fn parse_format_csv() {
2543        let stmt = parse(r#"RECALL episodic ABOUT "test" FORMAT csv"#).unwrap();
2544        match stmt {
2545            Statement::Recall(r) => {
2546                assert_eq!(r.result_format.unwrap(), OutputFormat::Csv);
2547            }
2548            _ => panic!("expected Recall"),
2549        }
2550    }
2551
2552    #[test]
2553    fn parse_group_by_with_no_results_still_parses() {
2554        // GROUP BY with empty results is a runtime concern, but parsing should succeed.
2555        let stmt =
2556            parse(r#"RECALL episodic ABOUT "nonexistent" GROUP BY entity_type COUNT LIMIT 0"#)
2557                .unwrap();
2558        match stmt {
2559            Statement::Recall(r) => {
2560                assert!(r.group_by.is_some());
2561                assert_eq!(r.limit, Some(0));
2562            }
2563            _ => panic!("expected Recall"),
2564        }
2565    }
2566
2567    #[test]
2568    fn parse_select_single_field() {
2569        let stmt = parse(r#"RECALL episodic ABOUT "test" SELECT id"#).unwrap();
2570        match stmt {
2571            Statement::Recall(r) => {
2572                assert_eq!(r.projection.unwrap(), vec!["id"]);
2573            }
2574            _ => panic!("expected Recall"),
2575        }
2576    }
2577
2578    #[test]
2579    fn parse_combined_group_by_and_format() {
2580        let stmt = parse(
2581            r#"RECALL episodic ABOUT "test" GROUP BY entity_type COUNT FORMAT json LIMIT 10"#,
2582        )
2583        .unwrap();
2584        match stmt {
2585            Statement::Recall(r) => {
2586                assert!(r.group_by.is_some());
2587                assert_eq!(r.result_format.unwrap(), OutputFormat::Json);
2588                assert_eq!(r.limit, Some(10));
2589            }
2590            _ => panic!("expected Recall"),
2591        }
2592    }
2593
2594    // ── Subqueries & Time-Travel ──
2595
2596    #[test]
2597    fn parse_as_of_clause() {
2598        let stmt =
2599            parse(r#"RECALL episodic ABOUT "deployment" AS OF "2026-03-01T12:00:00Z" LIMIT 5"#)
2600                .unwrap();
2601        match stmt {
2602            Statement::Recall(r) => {
2603                assert_eq!(
2604                    r.as_of.unwrap(),
2605                    RecallSnapshotAst::Unqualified("2026-03-01T12:00:00Z".to_string())
2606                );
2607                assert_eq!(r.limit, Some(5));
2608            }
2609            _ => panic!("expected Recall"),
2610        }
2611    }
2612
2613    #[test]
2614    fn parse_explicit_observed_as_of_clause() {
2615        let stmt = parse(
2616            r#"RECALL episodic ABOUT "deployment" AS OF OBSERVED "2026-03-01T12:00:00Z" LIMIT 5"#,
2617        )
2618        .unwrap();
2619        match stmt {
2620            Statement::Recall(r) => {
2621                assert_eq!(
2622                    r.as_of.unwrap(),
2623                    RecallSnapshotAst::Observed("2026-03-01T12:00:00Z".to_string())
2624                );
2625                assert_eq!(r.limit, Some(5));
2626            }
2627            _ => panic!("expected Recall"),
2628        }
2629    }
2630
2631    #[test]
2632    fn parse_recorded_as_of_clause() {
2633        let stmt = parse(
2634            r#"RECALL episodic ABOUT "deployment" AS OF RECORDED "2026-03-01T12:00:00Z" LIMIT 5"#,
2635        )
2636        .unwrap();
2637        match stmt {
2638            Statement::Recall(r) => {
2639                assert_eq!(
2640                    r.as_of.unwrap(),
2641                    RecallSnapshotAst::Recorded("2026-03-01T12:00:00Z".to_string())
2642                );
2643                assert_eq!(r.limit, Some(5));
2644            }
2645            _ => panic!("expected Recall"),
2646        }
2647    }
2648
2649    #[test]
2650    fn parse_revision_as_of_clause() {
2651        let stmt = parse(
2652            r#"RECALL semantic ABOUT "deployment" AS OF REVISION "01HW7N0Z5CH9R1R7Z4S4V5Y4QF" LIMIT 5"#,
2653        )
2654        .unwrap();
2655        match stmt {
2656            Statement::Recall(r) => {
2657                assert_eq!(
2658                    r.as_of.unwrap(),
2659                    RecallSnapshotAst::Revision("01HW7N0Z5CH9R1R7Z4S4V5Y4QF".to_string())
2660                );
2661                assert_eq!(r.limit, Some(5));
2662            }
2663            _ => panic!("expected Recall"),
2664        }
2665    }
2666
2667    #[test]
2668    fn parse_in_subquery() {
2669        let stmt = parse(
2670            r#"RECALL episodic ABOUT "outage" WHERE entity IN (RECALL semantic ABOUT "critical services") LIMIT 10"#,
2671        )
2672        .unwrap();
2673        match stmt {
2674            Statement::Recall(r) => {
2675                assert_eq!(r.subquery_filters.len(), 1);
2676                assert_eq!(r.subquery_filters[0].field, "entity");
2677                assert_eq!(r.subquery_filters[0].subquery.about, "critical services");
2678                assert_eq!(r.subquery_filters[0].subquery.layers, vec![Layer::Semantic]);
2679                assert_eq!(r.limit, Some(10));
2680            }
2681            _ => panic!("expected Recall"),
2682        }
2683    }
2684
2685    #[test]
2686    fn parse_subquery_with_temporal() {
2687        let stmt = parse(
2688            r#"RECALL episodic ABOUT "bugs" WHERE entity IN (RECALL episodic ABOUT "releases" AFTER "2026-01-01" LIMIT 5)"#,
2689        )
2690        .unwrap();
2691        match stmt {
2692            Statement::Recall(r) => {
2693                assert_eq!(r.subquery_filters.len(), 1);
2694                let sq = &r.subquery_filters[0].subquery;
2695                assert_eq!(sq.about, "releases");
2696                assert!(sq.temporal.is_some());
2697                assert_eq!(sq.limit, Some(5));
2698            }
2699            _ => panic!("expected Recall"),
2700        }
2701    }
2702
2703    #[test]
2704    fn parse_subquery_with_involving() {
2705        let stmt = parse(
2706            r#"RECALL semantic ABOUT "patterns" WHERE entity IN (RECALL episodic ABOUT "events" INVOLVING "auth", "db")"#,
2707        )
2708        .unwrap();
2709        match stmt {
2710            Statement::Recall(r) => {
2711                assert_eq!(r.subquery_filters.len(), 1);
2712                let sq = &r.subquery_filters[0].subquery;
2713                assert_eq!(
2714                    sq.involving.as_ref().unwrap(),
2715                    &vec!["auth".to_string(), "db".to_string()]
2716                );
2717            }
2718            _ => panic!("expected Recall"),
2719        }
2720    }
2721
2722    #[test]
2723    fn parse_as_of_with_where() {
2724        let stmt =
2725            parse(r#"RECALL episodic ABOUT "events" AS OF "2026-06-01" WHERE importance > 0.5"#)
2726                .unwrap();
2727        match stmt {
2728            Statement::Recall(r) => {
2729                assert_eq!(
2730                    r.as_of.unwrap(),
2731                    RecallSnapshotAst::Unqualified("2026-06-01".to_string())
2732                );
2733                assert_eq!(r.where_clauses.len(), 1);
2734                assert_eq!(r.where_clauses[0].field, "importance");
2735            }
2736            _ => panic!("expected Recall"),
2737        }
2738    }
2739
2740    #[test]
2741    fn parse_where_with_both_condition_and_subquery() {
2742        let stmt = parse(
2743            r#"RECALL episodic ABOUT "test" WHERE importance > 0.5 WHERE entity IN (RECALL semantic ABOUT "services")"#,
2744        )
2745        .unwrap();
2746        match stmt {
2747            Statement::Recall(r) => {
2748                assert_eq!(r.where_clauses.len(), 1);
2749                assert_eq!(r.subquery_filters.len(), 1);
2750            }
2751            _ => panic!("expected Recall"),
2752        }
2753    }
2754
2755    // ── TRAVERSE, Batch FORGET, Upsert REMEMBER ──
2756
2757    #[test]
2758    fn parse_traverse_minimal() {
2759        let stmt = parse(r#"TRAVERSE FROM "node1" DEPTH 3"#).unwrap();
2760        match stmt {
2761            Statement::Traverse(t) => {
2762                assert_eq!(t.from, "node1");
2763                assert_eq!(t.depth, 3);
2764                assert!(t.via.is_none());
2765                assert!(t.where_clauses.is_empty());
2766                assert!(t.limit.is_none());
2767            }
2768            other => panic!("expected Traverse, got {other:?}"),
2769        }
2770    }
2771
2772    #[test]
2773    fn parse_traverse_with_via() {
2774        let stmt =
2775            parse(r#"TRAVERSE FROM "concept_a" VIA causes, related_to DEPTH 2 LIMIT 10"#).unwrap();
2776        match stmt {
2777            Statement::Traverse(t) => {
2778                assert_eq!(t.from, "concept_a");
2779                assert_eq!(t.via.as_ref().unwrap(), &["causes", "related_to"]);
2780                assert_eq!(t.depth, 2);
2781                assert_eq!(t.limit, Some(10));
2782            }
2783            other => panic!("expected Traverse, got {other:?}"),
2784        }
2785    }
2786
2787    #[test]
2788    fn parse_traverse_with_where() {
2789        let stmt =
2790            parse(r#"TRAVERSE FROM "root" DEPTH 5 WHERE weight > 0.5 WHERE confidence > 0.3"#)
2791                .unwrap();
2792        match stmt {
2793            Statement::Traverse(t) => {
2794                assert_eq!(t.depth, 5);
2795                assert_eq!(t.where_clauses.len(), 2);
2796                assert_eq!(t.where_clauses[0].field, "weight");
2797                assert_eq!(t.where_clauses[1].field, "confidence");
2798            }
2799            other => panic!("expected Traverse, got {other:?}"),
2800        }
2801    }
2802
2803    #[test]
2804    fn roundtrip_traverse() {
2805        let q = r#"TRAVERSE FROM "node1" VIA causes DEPTH 3 LIMIT 10"#;
2806        let stmt1 = parse(q).unwrap();
2807        let rendered = stmt1.to_string();
2808        let stmt2 = parse(&rendered).unwrap();
2809        assert_eq!(stmt1, stmt2);
2810    }
2811
2812    // ── Parameter placeholder tests ────────────────────────────────────
2813
2814    #[test]
2815    fn parse_positional_param_in_about() {
2816        let stmt = parse(r#"RECALL episodic ABOUT $1 LIMIT 10"#).unwrap();
2817        match stmt {
2818            Statement::Recall(r) => assert_eq!(r.about, "$1"),
2819            _ => panic!("expected Recall"),
2820        }
2821    }
2822
2823    #[test]
2824    fn parse_named_param_in_about() {
2825        let stmt = parse(r#"RECALL episodic ABOUT $query LIMIT 5"#).unwrap();
2826        match stmt {
2827            Statement::Recall(r) => assert_eq!(r.about, "$query"),
2828            _ => panic!("expected Recall"),
2829        }
2830    }
2831
2832    #[test]
2833    fn parse_param_in_where_condition() {
2834        let stmt = parse(r#"RECALL episodic ABOUT "test" WHERE importance > $threshold"#).unwrap();
2835        match stmt {
2836            Statement::Recall(r) => {
2837                assert_eq!(r.where_clauses.len(), 1);
2838                assert_eq!(
2839                    r.where_clauses[0].value,
2840                    ConditionValue::Param("$threshold".into())
2841                );
2842            }
2843            _ => panic!("expected Recall"),
2844        }
2845    }
2846
2847    // ── EXPLAIN ──
2848
2849    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2850    enum ExpectedExplainInner {
2851        Recall,
2852        RecallEvents,
2853        Think,
2854        Correct,
2855        Supersede,
2856        MergeMemory,
2857        Retract,
2858        History,
2859        Traverse,
2860        Inspect,
2861        Trace,
2862        ExplainCauses,
2863        WhatIf,
2864        Counterfactual,
2865        ShowPolicies,
2866        ExplainPolicy,
2867    }
2868
2869    fn explain_inner_matches(statement: &Statement, expected: ExpectedExplainInner) -> bool {
2870        match expected {
2871            ExpectedExplainInner::Recall => matches!(statement, Statement::Recall(_)),
2872            ExpectedExplainInner::RecallEvents => matches!(statement, Statement::RecallEvents(_)),
2873            ExpectedExplainInner::Think => matches!(statement, Statement::Think(_)),
2874            ExpectedExplainInner::Correct => matches!(statement, Statement::Correct(_)),
2875            ExpectedExplainInner::Supersede => matches!(statement, Statement::Supersede(_)),
2876            ExpectedExplainInner::MergeMemory => matches!(statement, Statement::MergeMemory(_)),
2877            ExpectedExplainInner::Retract => matches!(statement, Statement::Retract(_)),
2878            ExpectedExplainInner::History => matches!(statement, Statement::History(_)),
2879            ExpectedExplainInner::Traverse => matches!(statement, Statement::Traverse(_)),
2880            ExpectedExplainInner::Inspect => matches!(statement, Statement::Inspect(_)),
2881            ExpectedExplainInner::Trace => matches!(statement, Statement::Trace(_)),
2882            ExpectedExplainInner::ExplainCauses => {
2883                matches!(statement, Statement::ExplainCauses(_))
2884            }
2885            ExpectedExplainInner::WhatIf => matches!(statement, Statement::WhatIf(_)),
2886            ExpectedExplainInner::Counterfactual => {
2887                matches!(statement, Statement::Counterfactual(_))
2888            }
2889            ExpectedExplainInner::ShowPolicies => matches!(statement, Statement::ShowPolicies(_)),
2890            ExpectedExplainInner::ExplainPolicy => {
2891                matches!(statement, Statement::ExplainPolicy(_))
2892            }
2893        }
2894    }
2895
2896    fn assert_explain_shape(
2897        query: &str,
2898        expected_analyze: bool,
2899        expected_inner: ExpectedExplainInner,
2900    ) {
2901        let stmt = parse(query).unwrap();
2902        match stmt {
2903            Statement::Explain(e) => {
2904                assert_eq!(
2905                    e.analyze, expected_analyze,
2906                    "unexpected analyze flag for `{query}`"
2907                );
2908                assert!(
2909                    explain_inner_matches(e.inner.as_ref(), expected_inner),
2910                    "unexpected inner statement for `{query}`: {:?}",
2911                    e.inner
2912                );
2913            }
2914            other => panic!("expected Explain, got {other:?} for `{query}`"),
2915        }
2916    }
2917
2918    #[test]
2919    fn parse_explain_statement_matrix() {
2920        for (query, expected_inner) in [
2921            (
2922                r#"EXPLAIN RECALL episodic ABOUT "test""#,
2923                ExpectedExplainInner::Recall,
2924            ),
2925            (
2926                r#"EXPLAIN RECALL EVENTS LIMIT 10"#,
2927                ExpectedExplainInner::RecallEvents,
2928            ),
2929            (
2930                r#"EXPLAIN THINK ABOUT "reasoning""#,
2931                ExpectedExplainInner::Think,
2932            ),
2933            (
2934                r#"EXPLAIN CORRECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "updated""#,
2935                ExpectedExplainInner::Correct,
2936            ),
2937            (
2938                r#"EXPLAIN SUPERSEDE "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "replacement""#,
2939                ExpectedExplainInner::Supersede,
2940            ),
2941            (
2942                r#"EXPLAIN RETRACT "01ARZ3NDEKTSV4RRFFQ69G5FAV" REASON "obsolete""#,
2943                ExpectedExplainInner::Retract,
2944            ),
2945            (
2946                r#"EXPLAIN HISTORY "01ARZ3NDEKTSV4RRFFQ69G5FAV" NAMESPACE custom"#,
2947                ExpectedExplainInner::History,
2948            ),
2949            (
2950                r#"EXPLAIN TRAVERSE FROM "01ARZ3NDEKTSV4RRFFQ69G5FAV" DEPTH 3"#,
2951                ExpectedExplainInner::Traverse,
2952            ),
2953            (
2954                r#"EXPLAIN INSPECT LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
2955                ExpectedExplainInner::Inspect,
2956            ),
2957            (
2958                r#"EXPLAIN TRACE LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
2959                ExpectedExplainInner::Trace,
2960            ),
2961            (
2962                r#"EXPLAIN EXPLAIN CAUSES "incident" DEPTH 2"#,
2963                ExpectedExplainInner::ExplainCauses,
2964            ),
2965            (
2966                r#"EXPLAIN WHAT_IF "increase timeout" THEN "fewer errors""#,
2967                ExpectedExplainInner::WhatIf,
2968            ),
2969            (
2970                r#"EXPLAIN COUNTERFACTUAL "cause" THEN "effect""#,
2971                ExpectedExplainInner::Counterfactual,
2972            ),
2973            (
2974                r#"EXPLAIN SHOW POLICIES FOR AGENT "agent-007""#,
2975                ExpectedExplainInner::ShowPolicies,
2976            ),
2977            (
2978                r#"EXPLAIN EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#,
2979                ExpectedExplainInner::ExplainPolicy,
2980            ),
2981        ] {
2982            assert_explain_shape(query, false, expected_inner);
2983        }
2984    }
2985
2986    #[test]
2987    fn parse_explain_analyze_statement_matrix() {
2988        for (query, expected_inner) in [
2989            (
2990                r#"EXPLAIN ANALYZE RECALL episodic ABOUT "test""#,
2991                ExpectedExplainInner::Recall,
2992            ),
2993            (
2994                r#"EXPLAIN ANALYZE RECALL EVENTS LIMIT 10"#,
2995                ExpectedExplainInner::RecallEvents,
2996            ),
2997            (
2998                r#"EXPLAIN ANALYZE THINK ABOUT "reasoning""#,
2999                ExpectedExplainInner::Think,
3000            ),
3001            (
3002                r#"EXPLAIN ANALYZE CORRECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "updated""#,
3003                ExpectedExplainInner::Correct,
3004            ),
3005            (
3006                r#"EXPLAIN ANALYZE SUPERSEDE "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "replacement""#,
3007                ExpectedExplainInner::Supersede,
3008            ),
3009            (
3010                r#"EXPLAIN ANALYZE MERGE MEMORY "01ARZ3NDEKTSV4RRFFQ69G5FAA" INTO "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3011                ExpectedExplainInner::MergeMemory,
3012            ),
3013            (
3014                r#"EXPLAIN ANALYZE RETRACT "01ARZ3NDEKTSV4RRFFQ69G5FAV" REASON "obsolete""#,
3015                ExpectedExplainInner::Retract,
3016            ),
3017            (
3018                r#"EXPLAIN ANALYZE HISTORY "01ARZ3NDEKTSV4RRFFQ69G5FAV" NAMESPACE custom"#,
3019                ExpectedExplainInner::History,
3020            ),
3021            (
3022                r#"EXPLAIN ANALYZE TRAVERSE FROM "01ARZ3NDEKTSV4RRFFQ69G5FAV" DEPTH 3"#,
3023                ExpectedExplainInner::Traverse,
3024            ),
3025            (
3026                r#"EXPLAIN ANALYZE INSPECT LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3027                ExpectedExplainInner::Inspect,
3028            ),
3029            (
3030                r#"EXPLAIN ANALYZE TRACE LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3031                ExpectedExplainInner::Trace,
3032            ),
3033            (
3034                r#"EXPLAIN ANALYZE EXPLAIN CAUSES "incident" DEPTH 2"#,
3035                ExpectedExplainInner::ExplainCauses,
3036            ),
3037            (
3038                r#"EXPLAIN ANALYZE WHAT_IF "increase timeout" THEN "fewer errors""#,
3039                ExpectedExplainInner::WhatIf,
3040            ),
3041            (
3042                r#"EXPLAIN ANALYZE COUNTERFACTUAL "cause" THEN "effect""#,
3043                ExpectedExplainInner::Counterfactual,
3044            ),
3045            (
3046                r#"EXPLAIN ANALYZE SHOW POLICIES FOR AGENT "agent-007""#,
3047                ExpectedExplainInner::ShowPolicies,
3048            ),
3049            (
3050                r#"EXPLAIN ANALYZE EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#,
3051                ExpectedExplainInner::ExplainPolicy,
3052            ),
3053        ] {
3054            assert_explain_shape(query, true, expected_inner);
3055        }
3056    }
3057
3058    #[test]
3059    fn parse_explain_rejects_non_wrappable_statement_classes() {
3060        for prefix in ["EXPLAIN", "EXPLAIN ANALYZE"] {
3061            for inner in [
3062                r#"REMEMBER episode CONTENT "data""#,
3063                "WATCH ALL FORMAT json",
3064                r#"CONNECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" TO "01ARZ3NDEKTSV4RRFFQ69G5FAA" AS causes"#,
3065                r#"GRANT recall ON NAMESPACE "default" TO AGENT "agent-007""#,
3066                r#"REVOKE forget ON NAMESPACE "sensitive" FROM AGENT "rogue""#,
3067                "SET TIER_POLICY semantic_archive_threshold = 0.2",
3068                r#"CONSOLIDATE WHERE episodic.access_count > 5"#,
3069                r#"CREATE REALM "analytics""#,
3070                r#"DROP REALM "analytics" CONFIRM"#,
3071                "SHOW CLUSTER",
3072            ] {
3073                let query = format!("{prefix} {inner}");
3074                assert!(
3075                    parse(&query).is_err(),
3076                    "`{query}` should be rejected by the EXPLAIN wrapper allowlist"
3077                );
3078            }
3079        }
3080    }
3081
3082    #[test]
3083    fn parse_modality_filter() {
3084        let stmt = parse(r#"RECALL episodic ABOUT "login page" MODALITY image, text"#).unwrap();
3085        match stmt {
3086            Statement::Recall(r) => {
3087                assert_eq!(r.about, "login page");
3088                let mods = r.modality.unwrap();
3089                assert_eq!(mods, vec!["image".to_string(), "text".to_string()]);
3090            }
3091            _ => panic!("expected Recall"),
3092        }
3093    }
3094
3095    #[test]
3096    fn parse_modality_single() {
3097        let stmt = parse(r#"RECALL episodic ABOUT "diagrams" MODALITY image"#).unwrap();
3098        match stmt {
3099            Statement::Recall(r) => {
3100                let mods = r.modality.unwrap();
3101                assert_eq!(mods, vec!["image".to_string()]);
3102            }
3103            _ => panic!("expected Recall"),
3104        }
3105    }
3106
3107    #[test]
3108    fn parse_recall_without_modality_is_none() {
3109        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3110        match stmt {
3111            Statement::Recall(r) => {
3112                assert!(r.modality.is_none());
3113            }
3114            _ => panic!("expected Recall"),
3115        }
3116    }
3117
3118    #[test]
3119    fn parse_modality_with_other_clauses() {
3120        let stmt = parse(
3121            r#"RECALL episodic ABOUT "query" WHERE importance > 0.5 MODALITY code, text LIMIT 10"#,
3122        )
3123        .unwrap();
3124        match stmt {
3125            Statement::Recall(r) => {
3126                let mods = r.modality.unwrap();
3127                assert_eq!(mods, vec!["code".to_string(), "text".to_string()]);
3128                assert_eq!(r.limit, Some(10));
3129                assert_eq!(r.where_clauses.len(), 1);
3130            }
3131            _ => panic!("expected Recall"),
3132        }
3133    }
3134
3135    #[test]
3136    fn parse_modality_extended_profiles() {
3137        let stmt = parse(
3138            r#"RECALL episodic ABOUT "artifact" MODALITY video, document, composite, external LIMIT 10"#,
3139        )
3140        .unwrap();
3141        match stmt {
3142            Statement::Recall(r) => {
3143                let mods = r.modality.unwrap();
3144                assert_eq!(
3145                    mods,
3146                    vec![
3147                        "video".to_string(),
3148                        "document".to_string(),
3149                        "composite".to_string(),
3150                        "external".to_string(),
3151                    ]
3152                );
3153            }
3154            _ => panic!("expected Recall"),
3155        }
3156    }
3157
3158    #[test]
3159    fn parse_resource_aware_recall_clauses() {
3160        let stmt = parse(
3161            r#"RECALL episodic ABOUT "artifact" MODALITY image RESOURCE_ROLE source, proof HYDRATION preview, full ARTIFACT preview, caption LIMIT 5"#,
3162        )
3163        .unwrap();
3164        match stmt {
3165            Statement::Recall(r) => {
3166                assert_eq!(r.modality.unwrap(), vec!["image".to_string()]);
3167                assert_eq!(
3168                    r.resource_roles.unwrap(),
3169                    vec!["source".to_string(), "proof".to_string()]
3170                );
3171                assert_eq!(
3172                    r.hydration_modes.unwrap(),
3173                    vec!["preview".to_string(), "full".to_string()]
3174                );
3175                assert_eq!(
3176                    r.artifact_kinds.unwrap(),
3177                    vec!["preview".to_string(), "caption".to_string()]
3178                );
3179            }
3180            _ => panic!("expected Recall"),
3181        }
3182    }
3183
3184    #[test]
3185    fn parse_recall_without_resource_aware_clauses_is_none() {
3186        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3187        match stmt {
3188            Statement::Recall(r) => {
3189                assert!(r.resource_roles.is_none());
3190                assert!(r.hydration_modes.is_none());
3191                assert!(r.artifact_kinds.is_none());
3192            }
3193            _ => panic!("expected Recall"),
3194        }
3195    }
3196
3197    // ── Realm & Policy statement parsing ──
3198
3199    #[test]
3200    fn parse_create_realm() {
3201        let stmt = parse(r#"CREATE REALM "analytics""#).unwrap();
3202        match stmt {
3203            Statement::CreateRealm(r) => {
3204                assert_eq!(r.name, "analytics");
3205                assert!(r.description.is_none());
3206            }
3207            _ => panic!("expected CreateRealm"),
3208        }
3209    }
3210
3211    #[test]
3212    fn parse_create_realm_with_description() {
3213        let stmt = parse(r#"CREATE REALM "analytics" DESCRIPTION "For analytics data""#).unwrap();
3214        match stmt {
3215            Statement::CreateRealm(r) => {
3216                assert_eq!(r.name, "analytics");
3217                assert_eq!(r.description.as_deref(), Some("For analytics data"));
3218            }
3219            _ => panic!("expected CreateRealm"),
3220        }
3221    }
3222
3223    #[test]
3224    fn parse_drop_realm() {
3225        let stmt = parse(r#"DROP REALM "analytics""#).unwrap();
3226        match stmt {
3227            Statement::DropRealm(r) => {
3228                assert_eq!(r.name, "analytics");
3229                assert!(!r.confirm);
3230            }
3231            _ => panic!("expected DropRealm"),
3232        }
3233    }
3234
3235    #[test]
3236    fn parse_drop_realm_confirm() {
3237        let stmt = parse(r#"DROP REALM "analytics" CONFIRM"#).unwrap();
3238        match stmt {
3239            Statement::DropRealm(r) => {
3240                assert_eq!(r.name, "analytics");
3241                assert!(r.confirm);
3242            }
3243            _ => panic!("expected DropRealm"),
3244        }
3245    }
3246
3247    #[test]
3248    fn parse_grant_single_action() {
3249        let stmt = parse(r#"GRANT recall ON NAMESPACE "default" TO AGENT "agent-007""#).unwrap();
3250        match stmt {
3251            Statement::Grant(g) => {
3252                assert_eq!(g.actions, vec!["recall"]);
3253                assert_eq!(g.target, GrantTarget::Namespace("default".into()));
3254                assert_eq!(g.principal, PrincipalRef::Agent("agent-007".into()));
3255            }
3256            _ => panic!("expected Grant"),
3257        }
3258    }
3259
3260    #[test]
3261    fn parse_grant_multiple_actions() {
3262        let stmt =
3263            parse(r#"GRANT recall, remember, think ON REALM "prod" TO TEAM "data-scientists""#)
3264                .unwrap();
3265        match stmt {
3266            Statement::Grant(g) => {
3267                assert_eq!(g.actions, vec!["recall", "remember", "think"]);
3268                assert_eq!(g.target, GrantTarget::Realm("prod".into()));
3269                assert_eq!(g.principal, PrincipalRef::Team("data-scientists".into()));
3270            }
3271            _ => panic!("expected Grant"),
3272        }
3273    }
3274
3275    #[test]
3276    fn parse_revoke() {
3277        let stmt = parse(r#"REVOKE forget ON NAMESPACE "sensitive" FROM AGENT "rogue""#).unwrap();
3278        match stmt {
3279            Statement::Revoke(r) => {
3280                assert_eq!(r.actions, vec!["forget"]);
3281                assert_eq!(r.target, GrantTarget::Namespace("sensitive".into()));
3282                assert_eq!(r.principal, PrincipalRef::Agent("rogue".into()));
3283            }
3284            _ => panic!("expected Revoke"),
3285        }
3286    }
3287
3288    #[test]
3289    fn parse_show_policies() {
3290        let stmt = parse(r#"SHOW POLICIES"#).unwrap();
3291        match stmt {
3292            Statement::ShowPolicies(s) => {
3293                assert!(s.principal.is_none());
3294            }
3295            _ => panic!("expected ShowPolicies"),
3296        }
3297    }
3298
3299    #[test]
3300    fn parse_show_policies_for_agent() {
3301        let stmt = parse(r#"SHOW POLICIES FOR AGENT "agent-007""#).unwrap();
3302        match stmt {
3303            Statement::ShowPolicies(s) => {
3304                assert_eq!(s.principal, Some(PrincipalRef::Agent("agent-007".into())));
3305            }
3306            _ => panic!("expected ShowPolicies"),
3307        }
3308    }
3309
3310    #[test]
3311    fn parse_explain_policy() {
3312        let stmt =
3313            parse(r#"EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#)
3314                .unwrap();
3315        match stmt {
3316            Statement::ExplainPolicy(e) => {
3317                assert_eq!(e.principal, PrincipalRef::Agent("agent-007".into()));
3318                assert_eq!(e.resource_type, "namespace");
3319                assert_eq!(e.resource_name, "default");
3320                assert_eq!(e.action, "recall");
3321            }
3322            _ => panic!("expected ExplainPolicy"),
3323        }
3324    }
3325
3326    #[test]
3327    fn parse_explain_policy_on_realm() {
3328        let stmt =
3329            parse(r#"EXPLAIN POLICY FOR TEAM "analysts" ON REALM "prod" ACTION remember"#).unwrap();
3330        match stmt {
3331            Statement::ExplainPolicy(e) => {
3332                assert_eq!(e.principal, PrincipalRef::Team("analysts".into()));
3333                assert_eq!(e.resource_type, "realm");
3334                assert_eq!(e.resource_name, "prod");
3335                assert_eq!(e.action, "remember");
3336            }
3337            _ => panic!("expected ExplainPolicy"),
3338        }
3339    }
3340
3341    #[test]
3342    fn roundtrip_create_realm() {
3343        let stmt = parse(r#"CREATE REALM "test-realm" DESCRIPTION "A test realm""#).unwrap();
3344        let display = stmt.to_string();
3345        assert!(display.contains("CREATE REALM"));
3346        assert!(display.contains("test-realm"));
3347        assert!(display.contains("DESCRIPTION"));
3348    }
3349
3350    #[test]
3351    fn roundtrip_grant() {
3352        let stmt = parse(r#"GRANT recall, think ON NAMESPACE "ns1" TO AGENT "a1""#).unwrap();
3353        let display = stmt.to_string();
3354        assert!(display.contains("GRANT recall, think"));
3355        assert!(display.contains("NAMESPACE"));
3356        assert!(display.contains("AGENT"));
3357    }
3358
3359    #[test]
3360    fn recall_events_basic() {
3361        let stmt = parse(r#"RECALL EVENTS WHERE event_type = "access_denied""#).unwrap();
3362        match stmt {
3363            Statement::RecallEvents(r) => {
3364                assert_eq!(r.where_clauses.len(), 1);
3365                assert_eq!(r.where_clauses[0].field, "event_type");
3366            }
3367            _ => panic!("expected RecallEvents"),
3368        }
3369    }
3370
3371    #[test]
3372    fn recall_events_multiple_filters() {
3373        let stmt = parse(
3374            r#"RECALL EVENTS WHERE agent = "agent-007" WHERE event_type = "access_denied" LIMIT 100"#,
3375        )
3376        .unwrap();
3377        match stmt {
3378            Statement::RecallEvents(r) => {
3379                assert_eq!(r.where_clauses.len(), 2);
3380                assert_eq!(r.limit, Some(100));
3381            }
3382            _ => panic!("expected RecallEvents"),
3383        }
3384    }
3385
3386    #[test]
3387    fn recall_events_with_temporal() {
3388        let stmt = parse(
3389            r#"RECALL EVENTS WHERE event_type = "policy_changed" AFTER "2026-01-01" LIMIT 50"#,
3390        )
3391        .unwrap();
3392        match stmt {
3393            Statement::RecallEvents(r) => {
3394                assert!(r.temporal.is_some());
3395                assert_eq!(r.limit, Some(50));
3396            }
3397            _ => panic!("expected RecallEvents"),
3398        }
3399    }
3400
3401    #[test]
3402    fn roundtrip_recall_events() {
3403        let stmt = parse(
3404            r#"RECALL EVENTS WHERE agent_id = "a1" WHERE event_type = "access_denied" NAMESPACE test LIMIT 10"#,
3405        )
3406        .unwrap();
3407        let display = stmt.to_string();
3408        assert!(display.contains("RECALL EVENTS"));
3409        assert!(display.contains("LIMIT 10"));
3410    }
3411
3412    // ── SHOW CLUSTER ─────────────────────────────
3413    #[test]
3414    fn parse_show_cluster() {
3415        let stmt = parse("SHOW CLUSTER").unwrap();
3416        assert!(matches!(stmt, Statement::ShowCluster));
3417    }
3418
3419    #[test]
3420    fn parse_show_cluster_status() {
3421        let stmt = parse("SHOW CLUSTER STATUS").unwrap();
3422        assert!(matches!(stmt, Statement::ShowCluster));
3423    }
3424
3425    #[test]
3426    fn parse_show_cluster_case_insensitive() {
3427        let stmt = parse("show cluster").unwrap();
3428        assert!(matches!(stmt, Statement::ShowCluster));
3429    }
3430
3431    #[test]
3432    fn roundtrip_show_cluster() {
3433        let stmt = parse("SHOW CLUSTER").unwrap();
3434        assert_eq!(stmt.to_string(), "SHOW CLUSTER");
3435    }
3436
3437    #[test]
3438    fn parse_recall_hybrid() {
3439        let q = r#"RECALL episodic ABOUT "semantic search" LIMIT 10 HYBRID"#;
3440        let stmt = parse(q).unwrap();
3441        if let Statement::Recall(r) = &stmt {
3442            assert!(r.hybrid);
3443            assert_eq!(r.about, "semantic search");
3444            assert_eq!(r.limit, Some(10));
3445        } else {
3446            panic!("expected Recall");
3447        }
3448    }
3449
3450    #[test]
3451    fn parse_recall_without_hybrid() {
3452        let q = r#"RECALL episodic ABOUT "query" LIMIT 5"#;
3453        let stmt = parse(q).unwrap();
3454        if let Statement::Recall(r) = &stmt {
3455            assert!(!r.hybrid);
3456        } else {
3457            panic!("expected Recall");
3458        }
3459    }
3460
3461    #[test]
3462    fn parse_recall_hybrid_case_insensitive() {
3463        let q = r#"RECALL episodic ABOUT "test" hybrid"#;
3464        let stmt = parse(q).unwrap();
3465        if let Statement::Recall(r) = &stmt {
3466            assert!(r.hybrid);
3467        } else {
3468            panic!("expected Recall");
3469        }
3470    }
3471
3472    #[test]
3473    fn roundtrip_recall_hybrid() {
3474        let q = r#"RECALL episodic ABOUT "hybrid test" LIMIT 10 HYBRID"#;
3475        let stmt1 = parse(q).unwrap();
3476        let rendered = stmt1.to_string();
3477        assert!(rendered.contains("HYBRID"));
3478        let stmt2 = parse(&rendered).unwrap();
3479        assert_eq!(stmt1, stmt2);
3480    }
3481
3482    // ── String unescape & injection tests ──────────────────
3483
3484    #[test]
3485    fn invalid_escape_sequence_returns_error() {
3486        let q = r#"RECALL episodic ABOUT "hello\qworld""#;
3487        let result = parse(q);
3488        assert!(result.is_err(), "expected parse error for \\q escape");
3489    }
3490
3491    #[test]
3492    fn valid_escape_newline_succeeds() {
3493        let q = r#"RECALL episodic ABOUT "hello\nworld""#;
3494        let stmt = parse(q).unwrap();
3495        if let Statement::Recall(r) = &stmt {
3496            assert_eq!(r.about, "hello\nworld");
3497        } else {
3498            panic!("expected Recall");
3499        }
3500    }
3501
3502    #[test]
3503    fn valid_escape_tab_succeeds() {
3504        let q = r#"RECALL episodic ABOUT "col1\tcol2""#;
3505        let stmt = parse(q).unwrap();
3506        if let Statement::Recall(r) = &stmt {
3507            assert_eq!(r.about, "col1\tcol2");
3508        } else {
3509            panic!("expected Recall");
3510        }
3511    }
3512
3513    #[test]
3514    fn valid_escape_backslash_succeeds() {
3515        let q = r#"RECALL episodic ABOUT "path\\to\\file""#;
3516        let stmt = parse(q).unwrap();
3517        if let Statement::Recall(r) = &stmt {
3518            assert_eq!(r.about, "path\\to\\file");
3519        } else {
3520            panic!("expected Recall");
3521        }
3522    }
3523
3524    #[test]
3525    fn valid_escape_quote_succeeds() {
3526        let q = r#"RECALL episodic ABOUT "say \"hello\"" "#;
3527        let stmt = parse(q).unwrap();
3528        if let Statement::Recall(r) = &stmt {
3529            assert_eq!(r.about, "say \"hello\"");
3530        } else {
3531            panic!("expected Recall");
3532        }
3533    }
3534
3535    // ── PPR/PageRank grammar tests ─────────────────────────
3536
3537    #[test]
3538    fn parse_recall_activation_ppr() {
3539        let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 3 ACTIVATION PPR"#;
3540        let stmt = parse(q).unwrap();
3541        if let Statement::Recall(r) = &stmt {
3542            let expand = r.expand.as_ref().expect("should have expand clause");
3543            assert_eq!(expand.activation, Some(ActivationModeAst::Ppr));
3544            assert_eq!(expand.depth, 3);
3545        } else {
3546            panic!("expected Recall");
3547        }
3548    }
3549
3550    #[test]
3551    fn parse_recall_activation_pagerank() {
3552        let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 2 ACTIVATION PAGERANK"#;
3553        let stmt = parse(q).unwrap();
3554        if let Statement::Recall(r) = &stmt {
3555            let expand = r.expand.as_ref().expect("should have expand clause");
3556            assert_eq!(expand.activation, Some(ActivationModeAst::Ppr));
3557            assert_eq!(expand.depth, 2);
3558        } else {
3559            panic!("expected Recall");
3560        }
3561    }
3562
3563    // ── Numeric parse error tests ──────────────────────────
3564
3565    #[test]
3566    fn limit_overflow_returns_error() {
3567        let q = r#"RECALL episodic ABOUT "test" LIMIT 99999999999999999999"#;
3568        let result = parse(q);
3569        assert!(result.is_err(), "expected error for overflow LIMIT");
3570    }
3571
3572    #[test]
3573    fn limit_valid_still_works() {
3574        let q = r#"RECALL episodic ABOUT "test" LIMIT 10"#;
3575        let stmt = parse(q).unwrap();
3576        if let Statement::Recall(r) = &stmt {
3577            assert_eq!(r.limit, Some(10));
3578        } else {
3579            panic!("expected Recall");
3580        }
3581    }
3582
3583    // ── Query limits tests ─────────────────────────────────
3584
3585    #[test]
3586    fn query_too_large_returns_error() {
3587        let limits = QueryLimits {
3588            max_query_length: 50,
3589            ..Default::default()
3590        };
3591        let q = &"x".repeat(100);
3592        let result = parse_with_limits(q, &limits);
3593        assert!(result.is_err());
3594        assert!(result.unwrap_err().message.contains("exceeds maximum"));
3595    }
3596
3597    #[test]
3598    fn expand_depth_exceeds_limit() {
3599        let limits = QueryLimits {
3600            max_expand_depth: 5,
3601            ..Default::default()
3602        };
3603        let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 6"#;
3604        let result = parse_with_limits(q, &limits);
3605        let err = result.unwrap_err();
3606        assert!(
3607            err.message.contains("DEPTH") || err.message.contains("depth"),
3608            "expected depth error, got: {}",
3609            err.message
3610        );
3611    }
3612
3613    #[test]
3614    fn limit_exceeds_max_returns_error() {
3615        let limits = QueryLimits {
3616            max_limit: 100,
3617            ..Default::default()
3618        };
3619        let q = r#"RECALL episodic ABOUT "test" LIMIT 200"#;
3620        let result = parse_with_limits(q, &limits);
3621        assert!(result.is_err());
3622        let msg = result.unwrap_err().message.to_lowercase();
3623        assert!(msg.contains("limit") || msg.contains("exceed"));
3624    }
3625
3626    #[test]
3627    fn normal_query_with_default_limits_succeeds() {
3628        let limits = QueryLimits::default();
3629        let q = r#"RECALL episodic ABOUT "test" LIMIT 100"#;
3630        let stmt = parse_with_limits(q, &limits).unwrap();
3631        if let Statement::Recall(r) = &stmt {
3632            assert_eq!(r.limit, Some(100));
3633        } else {
3634            panic!("expected Recall");
3635        }
3636    }
3637
3638    // ── DEPTH clause tests ─────────────────────────────────────────────
3639
3640    #[test]
3641    fn parse_recall_depth_auto() {
3642        let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH AUTO"#).unwrap();
3643        match stmt {
3644            Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Auto)),
3645            other => panic!("expected Recall, got {other:?}"),
3646        }
3647    }
3648
3649    #[test]
3650    fn parse_recall_depth_full() {
3651        let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH FULL"#).unwrap();
3652        match stmt {
3653            Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Full)),
3654            other => panic!("expected Recall, got {other:?}"),
3655        }
3656    }
3657
3658    #[test]
3659    fn parse_recall_depth_summary() {
3660        let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH SUMMARY"#).unwrap();
3661        match stmt {
3662            Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Summary)),
3663            other => panic!("expected Recall, got {other:?}"),
3664        }
3665    }
3666
3667    #[test]
3668    fn parse_think_depth_full() {
3669        let stmt = parse(r#"THINK ABOUT "test" DEPTH FULL"#).unwrap();
3670        match stmt {
3671            Statement::Think(t) => assert_eq!(t.depth_mode, Some(DepthModeAst::Full)),
3672            other => panic!("expected Think, got {other:?}"),
3673        }
3674    }
3675
3676    #[test]
3677    fn parse_recall_depth_omitted_is_none() {
3678        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3679        match stmt {
3680            Statement::Recall(r) => assert_eq!(r.depth_mode, None),
3681            other => panic!("expected Recall, got {other:?}"),
3682        }
3683    }
3684
3685    // ── WITH PROSPECTIVE clause tests ──────────────────────────────────
3686
3687    #[test]
3688    fn parse_recall_with_prospective_on() {
3689        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE ON"#).unwrap();
3690        match stmt {
3691            Statement::Recall(r) => assert_eq!(r.with_prospective, Some(true)),
3692            other => panic!("expected Recall, got {other:?}"),
3693        }
3694    }
3695
3696    #[test]
3697    fn parse_recall_with_prospective_off() {
3698        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE OFF"#).unwrap();
3699        match stmt {
3700            Statement::Recall(r) => assert_eq!(r.with_prospective, Some(false)),
3701            other => panic!("expected Recall, got {other:?}"),
3702        }
3703    }
3704
3705    #[test]
3706    fn parse_think_with_prospective_on() {
3707        let stmt = parse(r#"THINK ABOUT "test" WITH PROSPECTIVE ON"#).unwrap();
3708        match stmt {
3709            Statement::Think(t) => assert_eq!(t.with_prospective, Some(true)),
3710            other => panic!("expected Think, got {other:?}"),
3711        }
3712    }
3713
3714    // ── WITH MCFA_DEFENSE clause tests ─────────────────────────────────
3715
3716    #[test]
3717    fn parse_recall_with_mcfa_on() {
3718        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH MCFA_DEFENSE ON"#).unwrap();
3719        match stmt {
3720            Statement::Recall(r) => assert_eq!(r.with_mcfa, Some(true)),
3721            other => panic!("expected Recall, got {other:?}"),
3722        }
3723    }
3724
3725    #[test]
3726    fn parse_recall_with_mcfa_off() {
3727        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH MCFA_DEFENSE OFF"#).unwrap();
3728        match stmt {
3729            Statement::Recall(r) => assert_eq!(r.with_mcfa, Some(false)),
3730            other => panic!("expected Recall, got {other:?}"),
3731        }
3732    }
3733
3734    #[test]
3735    fn parse_think_with_mcfa_off() {
3736        let stmt = parse(r#"THINK ABOUT "test" WITH MCFA_DEFENSE OFF"#).unwrap();
3737        match stmt {
3738            Statement::Think(t) => assert_eq!(t.with_mcfa, Some(false)),
3739            other => panic!("expected Think, got {other:?}"),
3740        }
3741    }
3742
3743    // ── WITH CONFLICTS clause tests ────────────────────────────────────
3744
3745    #[test]
3746    fn parse_recall_with_conflicts() {
3747        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH CONFLICTS"#).unwrap();
3748        match stmt {
3749            Statement::Recall(r) => assert!(r.with_conflicts),
3750            other => panic!("expected Recall, got {other:?}"),
3751        }
3752    }
3753
3754    #[test]
3755    fn parse_recall_without_conflicts_default() {
3756        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3757        match stmt {
3758            Statement::Recall(r) => assert!(!r.with_conflicts),
3759            other => panic!("expected Recall, got {other:?}"),
3760        }
3761    }
3762
3763    // ── TOPIC clause tests (Story 5.2) ─────────────────────────────────
3764
3765    #[test]
3766    fn parse_recall_topic() {
3767        let stmt = parse(r#"RECALL episodic ABOUT "test" TOPIC "deployment""#).unwrap();
3768        match stmt {
3769            Statement::Recall(r) => {
3770                assert_eq!(r.topic, Some("deployment".to_string()));
3771            }
3772            other => panic!("expected Recall, got {other:?}"),
3773        }
3774    }
3775
3776    #[test]
3777    fn parse_recall_topic_with_temporal() {
3778        let stmt = parse(
3779            r#"RECALL episodic ABOUT "test" BETWEEN "2026-01-01" AND "2026-06-01" TOPIC "deployment""#,
3780        )
3781        .unwrap();
3782        match stmt {
3783            Statement::Recall(r) => {
3784                assert_eq!(r.topic, Some("deployment".to_string()));
3785                assert_eq!(
3786                    r.temporal,
3787                    Some(TemporalClause::Between {
3788                        start: "2026-01-01".into(),
3789                        end: "2026-06-01".into()
3790                    })
3791                );
3792            }
3793            other => panic!("expected Recall, got {other:?}"),
3794        }
3795    }
3796
3797    #[test]
3798    fn parse_recall_topic_omitted_is_none() {
3799        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3800        match stmt {
3801            Statement::Recall(r) => assert_eq!(r.topic, None),
3802            other => panic!("expected Recall, got {other:?}"),
3803        }
3804    }
3805
3806    // ── MODE ITERATIVE clause tests (Story 5.4) ───────────────────────
3807
3808    #[test]
3809    fn parse_think_mode_iterative() {
3810        let stmt = parse(r#"THINK ABOUT "test" BUDGET 4096 MODE ITERATIVE MAX_HOPS 3"#).unwrap();
3811        match stmt {
3812            Statement::Think(t) => {
3813                assert_eq!(t.mode, RetrievalMode::Iterative);
3814                assert_eq!(t.max_hops, Some(3));
3815                assert_eq!(t.budget, Some(4096));
3816            }
3817            other => panic!("expected Think, got {other:?}"),
3818        }
3819    }
3820
3821    #[test]
3822    fn parse_think_mode_iterative_without_max_hops() {
3823        let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE"#).unwrap();
3824        match stmt {
3825            Statement::Think(t) => {
3826                assert_eq!(t.mode, RetrievalMode::Iterative);
3827                assert_eq!(t.max_hops, None);
3828            }
3829            other => panic!("expected Think, got {other:?}"),
3830        }
3831    }
3832
3833    #[test]
3834    fn parse_think_mode_iterative_max_hops_validation_zero() {
3835        let result = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 0"#);
3836        assert!(result.is_err());
3837        let msg = result.unwrap_err().message;
3838        assert!(msg.contains("MAX_HOPS must be between 1 and 5"));
3839    }
3840
3841    #[test]
3842    fn parse_think_mode_iterative_max_hops_validation_too_high() {
3843        let result = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 6"#);
3844        assert!(result.is_err());
3845        let msg = result.unwrap_err().message;
3846        assert!(msg.contains("MAX_HOPS must be between 1 and 5"));
3847    }
3848
3849    #[test]
3850    fn parse_think_mode_iterative_max_hops_boundary_valid() {
3851        let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 1"#).unwrap();
3852        match stmt {
3853            Statement::Think(t) => assert_eq!(t.max_hops, Some(1)),
3854            other => panic!("expected Think, got {other:?}"),
3855        }
3856
3857        let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 5"#).unwrap();
3858        match stmt {
3859            Statement::Think(t) => assert_eq!(t.max_hops, Some(5)),
3860            other => panic!("expected Think, got {other:?}"),
3861        }
3862    }
3863
3864    #[test]
3865    fn parse_think_non_iterative_is_default() {
3866        let stmt = parse(r#"THINK ABOUT "test""#).unwrap();
3867        match stmt {
3868            Statement::Think(t) => {
3869                assert_eq!(t.mode, RetrievalMode::Local);
3870                assert_eq!(t.max_hops, None);
3871            }
3872            other => panic!("expected Think, got {other:?}"),
3873        }
3874    }
3875
3876    #[test]
3877    fn parse_think_mode_adaptive_clause() {
3878        let stmt = parse(r#"THINK ABOUT "test" MODE ADAPTIVE"#).unwrap();
3879        match stmt {
3880            Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Adaptive),
3881            other => panic!("expected Think, got {other:?}"),
3882        }
3883    }
3884
3885    #[test]
3886    fn parse_think_mode_raptor_clause() {
3887        let stmt = parse(r#"THINK ABOUT "test" MODE RAPTOR"#).unwrap();
3888        match stmt {
3889            Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Raptor),
3890            other => panic!("expected Think, got {other:?}"),
3891        }
3892    }
3893
3894    #[test]
3895    fn parse_think_mode_hybrid_clause() {
3896        let stmt = parse(r#"THINK ABOUT "test" MODE HYBRID"#).unwrap();
3897        match stmt {
3898            Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Hybrid),
3899            other => panic!("expected Think, got {other:?}"),
3900        }
3901    }
3902
3903    // ── EVENTS clause tests (Story 5.1) ───────────────────────────────
3904
3905    #[test]
3906    fn parse_recall_events_between() {
3907        let stmt = parse(r#"RECALL EVENTS BETWEEN "2026-03-01" AND "2026-03-15""#).unwrap();
3908        match stmt {
3909            Statement::RecallEvents(re) => {
3910                assert_eq!(
3911                    re.temporal,
3912                    Some(TemporalClause::Between {
3913                        start: "2026-03-01".into(),
3914                        end: "2026-03-15".into()
3915                    })
3916                );
3917                assert_eq!(re.entity_filter, None);
3918            }
3919            other => panic!("expected RecallEvents, got {other:?}"),
3920        }
3921    }
3922
3923    #[test]
3924    fn parse_recall_events_for_entity() {
3925        let stmt = parse(r#"RECALL EVENTS FOR "nginx""#).unwrap();
3926        match stmt {
3927            Statement::RecallEvents(re) => {
3928                assert_eq!(re.entity_filter, Some("nginx".to_string()));
3929            }
3930            other => panic!("expected RecallEvents, got {other:?}"),
3931        }
3932    }
3933
3934    #[test]
3935    fn parse_recall_events_for_entity_with_temporal() {
3936        let stmt =
3937            parse(r#"RECALL EVENTS FOR "nginx" BETWEEN "2026-03-01" AND "2026-03-15""#).unwrap();
3938        match stmt {
3939            Statement::RecallEvents(re) => {
3940                assert_eq!(re.entity_filter, Some("nginx".to_string()));
3941                assert_eq!(
3942                    re.temporal,
3943                    Some(TemporalClause::Between {
3944                        start: "2026-03-01".into(),
3945                        end: "2026-03-15".into()
3946                    })
3947                );
3948            }
3949            other => panic!("expected RecallEvents, got {other:?}"),
3950        }
3951    }
3952
3953    #[test]
3954    fn parse_recall_events_where_subject() {
3955        let stmt = parse(r#"RECALL EVENTS WHERE subject = "user_login""#).unwrap();
3956        match stmt {
3957            Statement::RecallEvents(re) => {
3958                assert_eq!(re.where_clauses.len(), 1);
3959                assert_eq!(re.where_clauses[0].field, "subject");
3960                assert_eq!(
3961                    re.where_clauses[0].value,
3962                    ConditionValue::String("user_login".into())
3963                );
3964            }
3965            other => panic!("expected RecallEvents, got {other:?}"),
3966        }
3967    }
3968
3969    #[test]
3970    fn parse_recall_events_for_with_where_and_limit() {
3971        let stmt = parse(r#"RECALL EVENTS FOR "nginx" WHERE verb = "crashed" LIMIT 10"#).unwrap();
3972        match stmt {
3973            Statement::RecallEvents(re) => {
3974                assert_eq!(re.entity_filter, Some("nginx".to_string()));
3975                assert_eq!(re.where_clauses.len(), 1);
3976                assert_eq!(re.where_clauses[0].field, "verb");
3977                assert_eq!(re.limit, Some(10));
3978            }
3979            other => panic!("expected RecallEvents, got {other:?}"),
3980        }
3981    }
3982
3983    // ── Combined clause tests ──────────────────────────────────────────
3984
3985    #[test]
3986    fn parse_recall_all_new_clauses() {
3987        let q = r#"
3988            RECALL episodic
3989              ABOUT "deployment"
3990              DEPTH FULL
3991              TOPIC "k8s"
3992              WITH PROSPECTIVE ON
3993              WITH MCFA_DEFENSE OFF
3994              WITH CONFLICTS
3995              LIMIT 20
3996        "#;
3997        let stmt = parse(q).unwrap();
3998        match stmt {
3999            Statement::Recall(r) => {
4000                assert_eq!(r.depth_mode, Some(DepthModeAst::Full));
4001                assert_eq!(r.topic, Some("k8s".to_string()));
4002                assert_eq!(r.with_prospective, Some(true));
4003                assert_eq!(r.with_mcfa, Some(false));
4004                assert!(r.with_conflicts);
4005                assert_eq!(r.limit, Some(20));
4006            }
4007            other => panic!("expected Recall, got {other:?}"),
4008        }
4009    }
4010
4011    #[test]
4012    fn parse_think_all_new_clauses() {
4013        let q = r#"
4014            THINK ABOUT "optimize HNSW"
4015              DEPTH SUMMARY
4016              WITH PROSPECTIVE OFF
4017              WITH MCFA_DEFENSE ON
4018              BUDGET 4096
4019              MODE ITERATIVE MAX_HOPS 2
4020              HYBRID
4021        "#;
4022        let stmt = parse(q).unwrap();
4023        match stmt {
4024            Statement::Think(t) => {
4025                assert_eq!(t.depth_mode, Some(DepthModeAst::Summary));
4026                assert_eq!(t.with_prospective, Some(false));
4027                assert_eq!(t.with_mcfa, Some(true));
4028                assert_eq!(t.mode, RetrievalMode::Iterative);
4029                assert_eq!(t.max_hops, Some(2));
4030                assert_eq!(t.budget, Some(4096));
4031                assert!(t.hybrid);
4032            }
4033            other => panic!("expected Think, got {other:?}"),
4034        }
4035    }
4036
4037    // ── Display round-trip tests ───────────────────────────────────────
4038
4039    #[test]
4040    fn display_recall_topic() {
4041        let q = r#"RECALL episodic ABOUT "test" TOPIC "deployment""#;
4042        let stmt = parse(q).unwrap();
4043        let displayed = stmt.to_string();
4044        assert!(
4045            displayed.contains("TOPIC \"deployment\""),
4046            "got: {displayed}"
4047        );
4048    }
4049
4050    #[test]
4051    fn display_think_mode_iterative() {
4052        let q = r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 3"#;
4053        let stmt = parse(q).unwrap();
4054        let displayed = stmt.to_string();
4055        assert!(displayed.contains("MODE iterative"), "got: {displayed}");
4056        assert!(displayed.contains("MAX_HOPS 3"), "got: {displayed}");
4057    }
4058
4059    #[test]
4060    fn display_recall_events_for() {
4061        let q = r#"RECALL EVENTS FOR "nginx""#;
4062        let stmt = parse(q).unwrap();
4063        let displayed = stmt.to_string();
4064        assert!(displayed.contains("FOR \"nginx\""), "got: {displayed}");
4065    }
4066
4067    #[test]
4068    fn display_recall_depth_mode() {
4069        let q = r#"RECALL episodic ABOUT "test" DEPTH FULL"#;
4070        let stmt = parse(q).unwrap();
4071        let displayed = stmt.to_string();
4072        assert!(displayed.contains("DEPTH FULL"), "got: {displayed}");
4073    }
4074
4075    #[test]
4076    fn display_recall_with_clauses() {
4077        let q = r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE ON WITH MCFA_DEFENSE OFF WITH CONFLICTS"#;
4078        let stmt = parse(q).unwrap();
4079        let displayed = stmt.to_string();
4080        assert!(
4081            displayed.contains("WITH PROSPECTIVE ON"),
4082            "got: {displayed}"
4083        );
4084        assert!(
4085            displayed.contains("WITH MCFA_DEFENSE OFF"),
4086            "got: {displayed}"
4087        );
4088        assert!(displayed.contains("WITH CONFLICTS"), "got: {displayed}");
4089    }
4090
4091    // ── MAX_HOPS only valid with ITERATIVE ─────────────────────────────
4092
4093    #[test]
4094    fn parse_think_max_hops_without_iterative_rejected() {
4095        // MAX_HOPS should not parse with non-iterative modes since grammar
4096        // only allows max_hops_clause after retrieval_mode within mode_clause.
4097        // MODE LOCAL MAX_HOPS 3 should fail at grammar level.
4098        let result = parse(r#"THINK ABOUT "test" MODE LOCAL MAX_HOPS 3"#);
4099        assert!(
4100            result.is_err(),
4101            "MAX_HOPS with LOCAL mode should be rejected"
4102        );
4103    }
4104
4105    // ── WITH PROVENANCE DEPTH clause tests (Story 4.2) ─────────────────
4106
4107    #[test]
4108    fn parse_recall_with_provenance_depth() {
4109        let stmt = parse(r#"RECALL semantic ABOUT "test" WITH PROVENANCE DEPTH 2"#).unwrap();
4110        match stmt {
4111            Statement::Recall(r) => {
4112                assert_eq!(r.provenance_depth, Some(2));
4113            }
4114            other => panic!("expected Recall, got {other:?}"),
4115        }
4116    }
4117
4118    #[test]
4119    fn parse_recall_provenance_depth_omitted_is_none() {
4120        let stmt = parse(r#"RECALL semantic ABOUT "test""#).unwrap();
4121        match stmt {
4122            Statement::Recall(r) => {
4123                assert_eq!(r.provenance_depth, None);
4124            }
4125            other => panic!("expected Recall, got {other:?}"),
4126        }
4127    }
4128
4129    #[test]
4130    fn parse_think_with_provenance_depth() {
4131        let stmt = parse(r#"THINK ABOUT "test" WITH PROVENANCE DEPTH 3"#).unwrap();
4132        match stmt {
4133            Statement::Think(t) => {
4134                assert_eq!(t.provenance_depth, Some(3));
4135            }
4136            other => panic!("expected Think, got {other:?}"),
4137        }
4138    }
4139
4140    #[test]
4141    fn parse_recall_provenance_with_conflicts_combo() {
4142        let stmt = parse(r#"RECALL episodic ABOUT "test" WITH CONFLICTS WITH PROVENANCE DEPTH 1"#)
4143            .unwrap();
4144        match stmt {
4145            Statement::Recall(r) => {
4146                assert!(r.with_conflicts);
4147                assert_eq!(r.provenance_depth, Some(1));
4148            }
4149            other => panic!("expected Recall, got {other:?}"),
4150        }
4151    }
4152
4153    // ── SET TIER_POLICY parse tests ─────────────────────────────────
4154
4155    #[test]
4156    fn parse_set_tier_policy_string_value() {
4157        let stmt = parse("SET TIER_POLICY working_to_episodic_ttl = '2h'").unwrap();
4158        match stmt {
4159            Statement::SetTierPolicy(s) => {
4160                assert_eq!(s.field, "working_to_episodic_ttl");
4161                assert_eq!(s.value, TierPolicyValue::Str("2h".into()));
4162            }
4163            other => panic!("expected SetTierPolicy, got {other:?}"),
4164        }
4165    }
4166
4167    #[test]
4168    fn parse_set_tier_policy_float_value() {
4169        let stmt = parse("SET TIER_POLICY episodic_to_semantic_threshold = 0.85").unwrap();
4170        match stmt {
4171            Statement::SetTierPolicy(s) => {
4172                assert_eq!(s.field, "episodic_to_semantic_threshold");
4173                assert_eq!(s.value, TierPolicyValue::Float(0.85));
4174            }
4175            other => panic!("expected SetTierPolicy, got {other:?}"),
4176        }
4177    }
4178
4179    #[test]
4180    fn parse_set_tier_policy_integer_value() {
4181        let stmt = parse("SET TIER_POLICY working_to_episodic_ttl = 3600").unwrap();
4182        match stmt {
4183            Statement::SetTierPolicy(s) => {
4184                assert_eq!(s.field, "working_to_episodic_ttl");
4185                assert_eq!(s.value, TierPolicyValue::Int(3600));
4186            }
4187            other => panic!("expected SetTierPolicy, got {other:?}"),
4188        }
4189    }
4190
4191    #[test]
4192    fn parse_set_tier_policy_case_insensitive() {
4193        let stmt = parse("set tier_policy procedural_min_success_rate = 0.5").unwrap();
4194        match stmt {
4195            Statement::SetTierPolicy(s) => {
4196                assert_eq!(s.field, "procedural_min_success_rate");
4197                assert_eq!(s.value, TierPolicyValue::Float(0.5));
4198            }
4199            other => panic!("expected SetTierPolicy, got {other:?}"),
4200        }
4201    }
4202
4203    #[test]
4204    fn parse_set_tier_policy_display_roundtrip() {
4205        let stmt = parse("SET TIER_POLICY semantic_archive_threshold = 0.2").unwrap();
4206        let display = format!("{stmt}");
4207        assert_eq!(display, "SET TIER_POLICY semantic_archive_threshold = 0.2");
4208    }
4209
4210    // ── FROM REALM clause tests (Story 6.2) ───────────────────────────
4211
4212    #[test]
4213    fn parse_recall_from_realm_single() {
4214        let stmt = parse(r#"RECALL episodic ABOUT "test" FROM REALM "production""#).unwrap();
4215        match stmt {
4216            Statement::Recall(r) => {
4217                assert_eq!(r.from_realms, Some(vec!["production".to_string()]));
4218            }
4219            other => panic!("expected Recall, got {other:?}"),
4220        }
4221    }
4222
4223    #[test]
4224    fn parse_recall_from_realm_multiple() {
4225        let stmt =
4226            parse(r#"RECALL episodic ABOUT "test" FROM REALM "production", "staging""#).unwrap();
4227        match stmt {
4228            Statement::Recall(r) => {
4229                assert_eq!(
4230                    r.from_realms,
4231                    Some(vec!["production".to_string(), "staging".to_string()])
4232                );
4233            }
4234            other => panic!("expected Recall, got {other:?}"),
4235        }
4236    }
4237
4238    #[test]
4239    fn parse_recall_without_from_realm() {
4240        let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
4241        match stmt {
4242            Statement::Recall(r) => assert!(r.from_realms.is_none()),
4243            other => panic!("expected Recall, got {other:?}"),
4244        }
4245    }
4246
4247    #[test]
4248    fn parse_recall_from_realm_display_roundtrip() {
4249        let stmt =
4250            parse(r#"RECALL episodic ABOUT "test" FROM REALM "production", "staging" LIMIT 10"#)
4251                .unwrap();
4252        let display = format!("{stmt}");
4253        assert!(display.contains("FROM REALM"));
4254        assert!(display.contains("\"production\""));
4255        assert!(display.contains("\"staging\""));
4256    }
4257
4258    // ── Pearl's 3-Rung Causal Statements ───────────────────────────────
4259
4260    #[test]
4261    fn parse_explain_causes_basic() {
4262        let stmt = parse(r#"EXPLAIN CAUSES "deployment failure""#).unwrap();
4263        match stmt {
4264            Statement::ExplainCauses(ec) => {
4265                assert_eq!(ec.target, "deployment failure");
4266                assert_eq!(ec.namespace, None);
4267                assert_eq!(ec.depth, None);
4268            }
4269            other => panic!("expected ExplainCauses, got {other:?}"),
4270        }
4271    }
4272
4273    #[test]
4274    fn parse_explain_causes_with_depth() {
4275        let stmt = parse(r#"EXPLAIN CAUSES "server crash" DEPTH 5"#).unwrap();
4276        match stmt {
4277            Statement::ExplainCauses(ec) => {
4278                assert_eq!(ec.target, "server crash");
4279                assert_eq!(ec.depth, Some(5));
4280            }
4281            other => panic!("expected ExplainCauses, got {other:?}"),
4282        }
4283    }
4284
4285    #[test]
4286    fn parse_explain_causes_with_namespace() {
4287        let stmt = parse(r#"EXPLAIN CAUSES "deployment failure" NAMESPACE ops"#).unwrap();
4288        match stmt {
4289            Statement::ExplainCauses(ec) => {
4290                assert_eq!(ec.target, "deployment failure");
4291                assert_eq!(ec.namespace, Some("ops".into()));
4292                assert_eq!(ec.depth, None);
4293            }
4294            other => panic!("expected ExplainCauses, got {other:?}"),
4295        }
4296    }
4297
4298    #[test]
4299    fn parse_explain_causes_full() {
4300        let stmt = parse(r#"EXPLAIN CAUSES "deployment failure" NAMESPACE ops DEPTH 3"#).unwrap();
4301        match stmt {
4302            Statement::ExplainCauses(ec) => {
4303                assert_eq!(ec.target, "deployment failure");
4304                assert_eq!(ec.namespace, Some("ops".into()));
4305                assert_eq!(ec.depth, Some(3));
4306            }
4307            other => panic!("expected ExplainCauses, got {other:?}"),
4308        }
4309    }
4310
4311    #[test]
4312    fn parse_what_if_basic() {
4313        let stmt = parse(r#"WHAT_IF "increase timeout" THEN "fewer errors""#).unwrap();
4314        match stmt {
4315            Statement::WhatIf(wi) => {
4316                assert_eq!(wi.intervention, "increase timeout");
4317                assert_eq!(wi.outcome, "fewer errors");
4318                assert_eq!(wi.namespace, None);
4319            }
4320            other => panic!("expected WhatIf, got {other:?}"),
4321        }
4322    }
4323
4324    #[test]
4325    fn parse_what_if_with_namespace() {
4326        let stmt =
4327            parse(r#"WHAT_IF "increase timeout" THEN "fewer errors" NAMESPACE prod"#).unwrap();
4328        match stmt {
4329            Statement::WhatIf(wi) => {
4330                assert_eq!(wi.intervention, "increase timeout");
4331                assert_eq!(wi.outcome, "fewer errors");
4332                assert_eq!(wi.namespace, Some("prod".into()));
4333            }
4334            other => panic!("expected WhatIf, got {other:?}"),
4335        }
4336    }
4337
4338    #[test]
4339    fn parse_counterfactual_basic() {
4340        let stmt = parse(r#"COUNTERFACTUAL "if deploy had not happened" THEN "outage""#).unwrap();
4341        match stmt {
4342            Statement::Counterfactual(cf) => {
4343                assert_eq!(cf.antecedent, "if deploy had not happened");
4344                assert_eq!(cf.consequent, "outage");
4345                assert_eq!(cf.namespace, None);
4346            }
4347            other => panic!("expected Counterfactual, got {other:?}"),
4348        }
4349    }
4350
4351    #[test]
4352    fn parse_counterfactual_with_namespace() {
4353        let stmt = parse(
4354            r#"COUNTERFACTUAL "if deploy had not happened" THEN "outage" NAMESPACE production"#,
4355        )
4356        .unwrap();
4357        match stmt {
4358            Statement::Counterfactual(cf) => {
4359                assert_eq!(cf.antecedent, "if deploy had not happened");
4360                assert_eq!(cf.consequent, "outage");
4361                assert_eq!(cf.namespace, Some("production".into()));
4362            }
4363            other => panic!("expected Counterfactual, got {other:?}"),
4364        }
4365    }
4366
4367    #[test]
4368    fn parse_explain_causes_display_roundtrip() {
4369        let stmt = parse(r#"EXPLAIN CAUSES "failure" NAMESPACE ops DEPTH 3"#).unwrap();
4370        let display = format!("{stmt}");
4371        assert!(display.contains("EXPLAIN CAUSES"));
4372        assert!(display.contains("failure"));
4373    }
4374
4375    #[test]
4376    fn parse_what_if_display_roundtrip() {
4377        let stmt = parse(r#"WHAT_IF "intervention" THEN "outcome""#).unwrap();
4378        let display = format!("{stmt}");
4379        assert!(display.contains("WHAT_IF"));
4380        assert!(display.contains("intervention"));
4381        assert!(display.contains("outcome"));
4382    }
4383
4384    #[test]
4385    fn parse_counterfactual_display_roundtrip() {
4386        let stmt = parse(r#"COUNTERFACTUAL "cause" THEN "effect""#).unwrap();
4387        let display = format!("{stmt}");
4388        assert!(display.contains("COUNTERFACTUAL"));
4389        assert!(display.contains("cause"));
4390        assert!(display.contains("effect"));
4391    }
4392}