1use std::cmp::Ordering;
24use std::collections::{BTreeMap, HashMap};
25
26use sha2::{Digest, Sha256};
27
28use super::ast::{
29 AddWithOption, AssembleStmt, BatchEntry, BatchStmt, CalQuery, CalStatement, CoalesceStmt,
30 Comparator, Condition, DescribeStmt, DescribeTarget, ExistsStmt, ExplainStmt, Extractor,
31 FormatClause, GrainTypePlural, HistoryStmt, LetBinding, PipelineStage, RecallStmt, SetOp,
32 SetOpStmt, Source, Value, WithOption,
33};
34use super::errors::{CalError, Span};
35use super::facade::{AssemblyManifest, CalStoreFacade};
36use crate::store_types::{AddOptions, DiversityConfig, RecallParams};
37use areev_core::error::{AreevError, Hash};
38
39#[derive(Debug, Clone)]
45pub struct CalExecutorConfig {
46 pub max_limit: u64,
49 pub default_limit: u64,
52 pub tier1_enabled: bool,
66 pub namespace_override: Option<String>,
71 pub user_id_override: Option<String>,
76 pub allow_destructive_ops: bool,
88 pub redact_budget_metadata: bool,
92 pub assembly_manifest_sample_rate: f64,
95 pub caller_scopes: Vec<String>,
99}
100
101impl Default for CalExecutorConfig {
102 fn default() -> Self {
103 Self {
104 max_limit: 1000,
105 default_limit: 50,
106 tier1_enabled: true,
107 allow_destructive_ops: true,
108 namespace_override: None,
109 user_id_override: None,
110 redact_budget_metadata: false,
111 assembly_manifest_sample_rate: 0.0,
112 caller_scopes: vec![],
113 }
114 }
115}
116
117#[derive(Debug, serde::Serialize)]
126pub struct CalExecResult {
127 pub query: String,
129 pub query_hash: String,
131 pub result: CalResultPayload,
133 pub warnings: Vec<String>,
135 pub anonymized: Option<serde_json::Value>,
139 pub metadata: CalMetadata,
141}
142
143impl CalExecResult {
144 pub fn payload_json(&self) -> serde_json::Result<serde_json::Value> {
159 let mut v = serde_json::to_value(&self.result)?;
160 if let Some(a) = &self.anonymized {
161 if let Some(obj) = v.as_object_mut() {
162 obj.insert("anonymized".into(), a.clone());
163 }
164 }
165 if !self.warnings.is_empty() {
166 if let Some(obj) = v.as_object_mut() {
170 obj.insert("warnings".into(), serde_json::json!(self.warnings));
171 }
172 }
173 Ok(v)
174 }
175}
176
177#[derive(Debug, serde::Serialize)]
179pub struct CalMetadata {
180 pub version: u32,
182 pub statement_type: String,
184 pub execution_time_ms: u64,
186 pub result_count: usize,
188}
189
190#[derive(Debug, serde::Serialize)]
192#[serde(tag = "type", rename_all = "snake_case")]
193pub enum CalResultPayload {
194 Grains {
196 grains: Vec<CalGrainResult>,
197 total_available: Option<usize>,
199 },
200 Exists { exists: bool, hash: String },
202 Count { count: usize },
204 Granted { principal: String, object: String, hash: String },
206 Revoked { principal: String, grants_touched: usize },
208 GrantList { grants: Vec<serde_json::Value> },
210 LoopRan { run: serde_json::Value },
212 Reviewed { hash: String, decision: String },
214 RecApplied { hash: String, rollbackable: bool },
216 RecRolledBack { hash: String },
218 Remembered { hash: String },
220 EntityAt { grain: Option<serde_json::Value>, axis: String, at_ms: i64 },
222 RunTrace { run_id: String, trace: serde_json::Value },
224 RunsTouching { hash: String, runs: Vec<String> },
226 DerivedFrom { hash: String, grains: Vec<serde_json::Value> },
228 Forks { forks: Vec<serde_json::Value> },
230 Merged { hash: String, subject: String, relation: String, object: String },
232 RelatedEntities { start: String, entities: Vec<String> },
234 NoveltyMatches { matches: Vec<serde_json::Value> },
236 History { versions: Vec<CalVersionResult> },
238 Describe { info: serde_json::Value },
240 Explain { plan: CalQueryPlan },
242 Batch {
244 results: HashMap<String, CalResultPayload>,
245 },
246 Assembled {
248 grains: Vec<CalGrainResult>,
250 sources: Vec<super::assemble::SourceMeta>,
252 total_tokens: u32,
254 budget_limit: Option<u32>,
256 progressive: bool,
258 total_available: Option<usize>,
260 },
261 Diff {
263 source_hash: String,
265 target_hash: String,
267 changes: Vec<super::ast::FieldDiff>,
269 },
270 Formatted {
273 text: String,
275 format: String,
277 grain_count: usize,
279 #[serde(skip_serializing)]
281 grains: Vec<CalGrainResult>,
282 },
283 MultiFormatted {
286 formats: HashMap<String, String>,
288 grain_count: usize,
290 #[serde(skip_serializing)]
292 grains: Vec<CalGrainResult>,
293 },
294 Added {
296 hash: String,
297 grain_type: String,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 extracted_count: Option<usize>,
301 #[serde(skip_serializing_if = "Vec::is_empty")]
303 extraction_warnings: Vec<String>,
304 },
305 Superseded { old_hash: String, new_hash: String },
307 Accumulated {
309 old_hash: String,
310 new_hash: String,
311 deltas: Vec<AccumulatedDelta>,
312 },
313 TemplateDefined { name: String },
315 TemplateDropped { name: String },
317 QueryDefined { name: String },
319 QueryDropped { name: String },
321 StreamAssemble {
323 assemble: Box<super::ast::AssembleStmt>,
325 with_options: Vec<super::ast::WithOption>,
327 },
328 Forgotten { target: String, count: u64 },
330 Purged { count: usize },
332 SubjectReport {
335 subject: String,
336 identity_names: Vec<String>,
337 grains: Vec<serde_json::Value>,
338 },
339 Unsupported { statement: String, message: String },
341}
342
343#[derive(Debug, Clone, serde::Serialize)]
345pub struct CalGrainResult {
346 pub hash: String,
348 pub grain_type: String,
350 pub score: f64,
352 pub fields: serde_json::Value,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub score_breakdown: Option<serde_json::Value>,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub explanation: Option<String>,
360 #[serde(skip_serializing_if = "Option::is_none")]
363 pub relative_time: Option<String>,
364 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
370 pub is_deterministic: bool,
371 #[serde(skip_serializing_if = "Option::is_none")]
379 pub contested_by: Option<Vec<String>>,
380}
381
382#[derive(Debug, Clone, serde::Serialize)]
384pub struct AccumulatedDelta {
385 pub field: String,
386 pub old_value: f64,
387 pub new_value: f64,
388}
389
390#[derive(Debug, serde::Serialize)]
392pub struct CalVersionResult {
393 pub hash: String,
395 pub object: String,
397 pub created_at: i64,
399 pub confidence: f64,
401 pub superseded_by: Option<String>,
403}
404
405#[derive(Debug, serde::Serialize)]
407pub struct CalQueryPlan {
408 pub statement_type: String,
410 pub grain_type: Option<String>,
412 pub query_routing: String,
414 pub index_usage: Vec<String>,
416 pub estimated_cost: String,
418 pub filters: Vec<String>,
420 pub pipeline: Vec<String>,
422}
423
424const MAX_LET_BINDINGS: usize = 5;
430
431const MAX_GRAINS_PER_LET: usize = 1000;
433
434#[derive(Debug, Clone)]
440pub enum LetValue {
441 Grains(Vec<CalGrainResult>),
443 Extracted(Vec<String>),
445}
446
447#[derive(Debug)]
454pub struct LetScope {
455 bindings: HashMap<String, LetValue>,
456}
457
458impl LetScope {
459 fn to_values(&self) -> HashMap<String, Vec<String>> {
466 self.bindings
467 .iter()
468 .map(|(name, value)| {
469 let values = match value {
470 LetValue::Extracted(v) => v.clone(),
471 LetValue::Grains(g) => g.iter().map(|r| r.hash.clone()).collect(),
472 };
473 (name.clone(), values)
474 })
475 .collect()
476 }
477}
478
479impl LetScope {
480 pub fn evaluate(
487 let_bindings: &[LetBinding],
488 executor: &CalExecutor,
489 store: &dyn CalStoreFacade,
490 query: &CalQuery,
491 warnings: &mut Vec<String>,
492 ) -> std::result::Result<Self, CalError> {
493 if let_bindings.len() > MAX_LET_BINDINGS {
494 return Err(CalError::TooManyLetBindings {
495 count: let_bindings.len(),
496 max: MAX_LET_BINDINGS,
497 span: let_bindings.first().and_then(|b| b.span),
498 });
499 }
500
501 let mut scope = LetScope {
502 bindings: HashMap::new(),
503 };
504
505 for binding in let_bindings {
506 if scope.bindings.contains_key(&binding.name) {
508 return Err(CalError::DuplicateParameter {
509 name: binding.name.clone(),
510 span: binding.span,
511 });
512 }
513
514 let surrogate = CalQuery {
516 version: query.version,
517 statement: (*binding.source).clone(),
518 pipeline: Vec::new(),
519 with_options: Vec::new(),
520 format: None,
521 let_bindings: Vec::new(),
522 let_values: scope.to_values(),
523 user_vars: HashMap::new(),
524 warnings: Vec::new(),
525 };
526
527 let payload =
528 executor.execute_statement(&surrogate.statement, store, &surrogate, warnings)?;
529
530 let grains = extract_grains(payload);
532
533 let grains = if grains.len() > MAX_GRAINS_PER_LET {
535 warnings.push(format!(
536 "LET ${} produced {} grains (capped to {})",
537 binding.name,
538 grains.len(),
539 MAX_GRAINS_PER_LET
540 ));
541 grains.into_iter().take(MAX_GRAINS_PER_LET).collect()
542 } else {
543 grains
544 };
545
546 let value = match binding.extractor {
548 Extractor::Subjects => {
549 let values: Vec<String> = grains
550 .iter()
551 .filter_map(|g| {
552 json_field(&g.fields, "subject")
553 .and_then(|v| v.as_str())
554 .map(|s| s.to_string())
555 })
556 .collect();
557 LetValue::Extracted(values)
558 }
559 Extractor::Objects => {
560 let values: Vec<String> = grains
561 .iter()
562 .filter_map(|g| {
563 json_field(&g.fields, "object")
564 .and_then(|v| v.as_str())
565 .map(|s| s.to_string())
566 })
567 .collect();
568 LetValue::Extracted(values)
569 }
570 Extractor::Hashes => {
571 let values: Vec<String> = grains.iter().map(|g| g.hash.clone()).collect();
572 LetValue::Extracted(values)
573 }
574 };
575
576 scope.bindings.insert(binding.name.clone(), value);
577 }
578
579 Ok(scope)
580 }
581
582 pub fn resolve(&self, name: &str) -> std::result::Result<&LetValue, CalError> {
586 self.bindings
587 .get(name)
588 .ok_or_else(|| CalError::UnboundParameter {
589 name: name.to_string(),
590 span: None,
591 })
592 }
593}
594
595impl Drop for LetScope {
597 fn drop(&mut self) {
598 self.bindings.clear();
599 self.bindings.shrink_to_fit();
600 }
601}
602
603const PLAN_CACHE_MAX: usize = 256;
614
615pub struct CalExecutor {
620 config: CalExecutorConfig,
621 plan_cache: std::sync::Mutex<HashMap<String, crate::ast::CalQuery>>,
639 governance: Option<std::sync::Arc<dyn crate::governance::GovernanceHost>>,
644}
645
646impl CalExecutor {
647 pub fn new(config: CalExecutorConfig) -> Self {
649 Self { config, governance: None, plan_cache: Default::default() }
650 }
651
652 pub fn parse_cached(
662 &self,
663 input: &str,
664 ) -> std::result::Result<crate::ast::CalQuery, CalError> {
665 if let Ok(cache) = self.plan_cache.lock() {
666 if let Some(q) = cache.get(input) {
667 return Ok(q.clone());
668 }
669 }
670 let query = crate::parser::parse(input)?;
671 if let Ok(mut cache) = self.plan_cache.lock() {
672 if cache.len() >= PLAN_CACHE_MAX {
673 cache.clear();
674 }
675 cache.insert(input.to_string(), query.clone());
676 }
677 Ok(query)
678 }
679
680 pub fn plan_cache_len(&self) -> usize {
683 self.plan_cache.lock().map(|c| c.len()).unwrap_or(0)
684 }
685
686 pub fn with_governance(
689 mut self,
690 host: std::sync::Arc<dyn crate::governance::GovernanceHost>,
691 ) -> Self {
692 self.governance = Some(host);
693 self
694 }
695
696 pub fn config(&self) -> &CalExecutorConfig {
698 &self.config
699 }
700
701 pub fn with_defaults() -> Self {
703 Self::new(CalExecutorConfig::default())
704 }
705
706 fn check_caller_scope(&self, stmt: &CalStatement) -> std::result::Result<(), CalError> {
710 let scopes = &self.config.caller_scopes;
711 if scopes.is_empty() {
712 return Ok(()); }
714 let required = required_scope_for_statement(stmt);
715 if scopes.iter().any(|s| s == "admin") {
717 return Ok(());
718 }
719 if scopes.iter().any(|s| s == required) {
720 return Ok(());
721 }
722 if required == "read" && scopes.iter().any(|s| s == "write") {
724 return Ok(());
725 }
726 Err(CalError::InsufficientScope {
727 required: required.to_string(),
728 statement: statement_type_name(stmt),
729 })
730 }
731
732 pub fn execute(
747 &self,
748 input: &str,
749 store: &dyn CalStoreFacade,
750 ) -> std::result::Result<CalExecResult, CalError> {
751 let start = std::time::Instant::now();
752
753 let query = self.parse_cached(input)?;
756
757 let query_hash = compute_query_hash(input);
759
760 let mut query = query;
768 let mut exec_warnings: Vec<String> = Vec::new();
769 if !query.let_bindings.is_empty() {
770 let scope = LetScope::evaluate(
771 &query.let_bindings,
772 self,
773 store,
774 &query,
775 &mut exec_warnings,
776 )?;
777 query.let_values = scope.to_values();
778 }
779
780 if let CalStatement::Recall(ref r) = query.statement {
783 Self::validate_pipeline_fields(&query.pipeline, &r.grain_type)?;
784 }
785
786 let payload =
788 self.execute_statement(&query.statement, store, &query, &mut exec_warnings)?;
789
790 let (payload, grouped_by) = self.apply_pipeline(payload, &query.pipeline, &mut exec_warnings)?;
792
793 let payload = apply_format_clause(
795 payload,
796 &query.format,
797 grouped_by.as_deref(),
798 RenderInputs {
799 user_vars: &query.user_vars,
800 store,
801 disclosure: disclosure_of(&query.with_options),
802 },
803 None,
804 &mut exec_warnings,
805 )?;
806
807 let mut warnings: Vec<String> = query.warnings.iter().map(|w| w.to_string()).collect();
809 warnings.extend(exec_warnings);
810
811 let elapsed_ms = start.elapsed().as_millis() as u64;
813 let stmt_type = statement_type_name(&query.statement);
814 let result_count = count_payload_results(&payload);
815
816 Ok(CalExecResult {
817 query: input.to_string(),
818 query_hash,
819 result: payload,
820 warnings,
821 anonymized: store.anon_egress_report(),
822 metadata: CalMetadata {
823 version: query.version.0,
824 statement_type: stmt_type,
825 execution_time_ms: elapsed_ms,
826 result_count,
827 },
828 })
829 }
830
831 pub fn execute_parsed(
838 &self,
839 query: crate::ast::CalQuery,
840 original_text: &str,
841 store: &dyn CalStoreFacade,
842 ) -> std::result::Result<CalExecResult, CalError> {
843 let start = std::time::Instant::now();
844
845 let query_hash = compute_query_hash(original_text);
847
848 let mut query = query;
851 let mut exec_warnings: Vec<String> = Vec::new();
852 if !query.let_bindings.is_empty() {
853 let scope = LetScope::evaluate(
854 &query.let_bindings,
855 self,
856 store,
857 &query,
858 &mut exec_warnings,
859 )?;
860 query.let_values = scope.to_values();
861 }
862
863 if let CalStatement::Recall(ref r) = query.statement {
865 Self::validate_pipeline_fields(&query.pipeline, &r.grain_type)?;
866 }
867
868 let payload =
870 self.execute_statement(&query.statement, store, &query, &mut exec_warnings)?;
871
872 let (payload, grouped_by) = self.apply_pipeline(payload, &query.pipeline, &mut exec_warnings)?;
874
875 let payload = apply_format_clause(
877 payload,
878 &query.format,
879 grouped_by.as_deref(),
880 RenderInputs {
881 user_vars: &query.user_vars,
882 store,
883 disclosure: disclosure_of(&query.with_options),
884 },
885 None,
886 &mut exec_warnings,
887 )?;
888
889 let mut warnings: Vec<String> = query.warnings.iter().map(|w| w.to_string()).collect();
891 warnings.extend(exec_warnings);
892
893 let elapsed_ms = start.elapsed().as_millis() as u64;
895 let stmt_type = statement_type_name(&query.statement);
896 let result_count = count_payload_results(&payload);
897
898 Ok(CalExecResult {
899 query: original_text.to_string(),
900 query_hash,
901 result: payload,
902 warnings,
903 anonymized: store.anon_egress_report(),
904 metadata: CalMetadata {
905 version: query.version.0,
906 statement_type: stmt_type,
907 execution_time_ms: elapsed_ms,
908 result_count,
909 },
910 })
911 }
912
913 fn inject_write_namespace(
926 &self,
927 fields: &mut serde_json::Map<String, serde_json::Value>,
928 store: &dyn CalStoreFacade,
929 ) {
930 if let Some(ref ns) = self.config.namespace_override {
931 fields.insert("namespace".into(), serde_json::Value::String(ns.clone()));
932 } else if !fields.contains_key("namespace") {
933 if let Some(ns) = store.default_namespace() {
934 fields.insert(
935 "namespace".into(),
936 serde_json::Value::String(ns.to_string()),
937 );
938 }
939 }
940 }
941
942 pub(super) fn execute_statement_internal(
943 &self,
944 stmt: &CalStatement,
945 store: &dyn CalStoreFacade,
946 query: &CalQuery,
947 exec_warnings: &mut Vec<String>,
948 ) -> std::result::Result<CalResultPayload, CalError> {
949 self.execute_statement(stmt, store, query, exec_warnings)
950 }
951
952 pub fn execute_statement(
953 &self,
954 stmt: &CalStatement,
955 store: &dyn CalStoreFacade,
956 query: &CalQuery,
957 exec_warnings: &mut Vec<String>,
958 ) -> std::result::Result<CalResultPayload, CalError> {
959 self.check_caller_scope(stmt)?;
961
962 match stmt {
963 CalStatement::Recall(recall) => {
964 self.execute_recall(recall, store, query, exec_warnings)
965 }
966 CalStatement::Exists(exists) => {
967 self.execute_exists(exists, store, exec_warnings, &query.let_values)
968 }
969 CalStatement::History(history) => {
970 self.execute_history(history, store, exec_warnings, &query.let_values)
971 }
972 CalStatement::Describe(describe) => self.execute_describe(describe, store),
973 CalStatement::Explain(explain) => self.execute_explain(explain, store, query),
974 CalStatement::Batch(batch) => self.execute_batch(batch, store, exec_warnings),
975 CalStatement::Coalesce(coalesce) => {
976 self.execute_coalesce(coalesce, store, query, exec_warnings)
977 }
978 CalStatement::SetOp(set_op) => self.execute_set_op(set_op, store, query, exec_warnings),
979 CalStatement::Assemble(assemble) => {
980 self.execute_assemble(assemble, store, query, exec_warnings)
981 }
982
983 CalStatement::Add(add) => {
985 if !self.config.tier1_enabled {
986 return Err(CalError::Tier1NotEnabled {
987 statement: "ADD".into(),
988 span: add.span,
989 });
990 }
991 let add_type = add.grain_type.as_str();
998 if !areev_core::types::registry::add_via_set_names().any(|n| n == add_type) {
999 let shapeable: Vec<&str> = areev_core::types::registry::add_via_set_names().collect();
1000 return Ok(CalResultPayload::Unsupported {
1001 statement: "add".into(),
1002 message: format!(
1003 "Grain type '{}' cannot be created via ADD. Addable types: {}.",
1004 add_type,
1005 shapeable.join(", ")
1006 ),
1007 });
1008 }
1009 for fa in &add.fields {
1011 if let super::ast::Value::Parameter { name } = &fa.value {
1012 return Ok(CalResultPayload::Unsupported {
1013 statement: "add".into(),
1014 message: format!(
1015 "Unresolved parameter ${} in SET clause. Parameters must be bound via LET.",
1016 name
1017 ),
1018 });
1019 }
1020 }
1021 let mut fields = serde_json::Map::new();
1022 for fa in &add.fields {
1023 fields.insert(fa.field.clone(), cal_value_to_json(&fa.value));
1024 }
1025 if !add.reason.is_empty() {
1027 fields.insert(
1028 "add_reason".into(),
1029 serde_json::Value::String(add.reason.clone()),
1030 );
1031 }
1032 if add
1040 .with_options
1041 .iter()
1042 .any(|o| matches!(o, super::ast::AddWithOption::Occurrence))
1043 {
1044 exec_warnings.push(
1045 "CAL-W014: WITH occurrence is inert here — tool occurrences are \
1046 recorded via record_tool_call (every binding), which stamps the \
1047 per-call identity that keeps retries distinct."
1048 .to_string(),
1049 );
1050 }
1051 self.inject_write_namespace(&mut fields, store);
1052 let options = build_add_options(&add.with_options, exec_warnings);
1054 match store.cal_add_with_options(add.grain_type.as_str(), &fields, options) {
1055 Ok(result) => Ok(CalResultPayload::Added {
1056 hash: hex::encode(result.hash.as_bytes()),
1057 grain_type: add.grain_type.as_str().to_string(),
1058 extracted_count: if result.extracted_count > 0 {
1059 Some(result.extracted_count)
1060 } else {
1061 None
1062 },
1063 extraction_warnings: result.extraction_warnings,
1064 }),
1065 Err(e) => Ok(CalResultPayload::Unsupported {
1066 statement: "add".into(),
1067 message: format!("ADD failed: {e}"),
1068 }),
1069 }
1070 }
1071 CalStatement::Supersede(sup) => {
1072 if !self.config.tier1_enabled {
1073 return Err(CalError::Tier1NotEnabled {
1074 statement: "SUPERSEDE".into(),
1075 span: sup.span,
1076 });
1077 }
1078 let raw_hash = sup.hash.strip_prefix("sha256:").unwrap_or(&sup.hash);
1080 let old_hash = match Hash::from_hex(raw_hash) {
1081 Ok(h) => h,
1082 Err(e) => {
1083 return Ok(CalResultPayload::Unsupported {
1084 statement: "supersede".into(),
1085 message: format!("invalid hash: {e}"),
1086 })
1087 }
1088 };
1089 let old_grain = match store.get(&old_hash) {
1091 Ok(g) => g,
1092 Err(e) => {
1093 return Ok(CalResultPayload::Unsupported {
1094 statement: "supersede".into(),
1095 message: format!("cannot retrieve grain for SUPERSEDE: {e}"),
1096 })
1097 }
1098 };
1099 let grain_type_str = old_grain.grain_type.as_str();
1100 let mut fields: serde_json::Map<String, serde_json::Value> =
1102 old_grain.fields.into_iter().collect();
1103 for sc in &sup.set_clauses {
1104 fields.insert(sc.field.clone(), cal_value_to_json(&sc.value));
1105 }
1106 if !sup.reason.is_empty() {
1108 fields.insert(
1109 "supersede_reason".into(),
1110 serde_json::Value::String(sup.reason.clone()),
1111 );
1112 }
1113 match store.cal_supersede(&old_hash, grain_type_str, &fields) {
1114 Ok(new_hash) => Ok(CalResultPayload::Superseded {
1115 old_hash: hex::encode(old_hash.as_bytes()),
1116 new_hash: hex::encode(new_hash.as_bytes()),
1117 }),
1118 Err(e) => Ok(CalResultPayload::Unsupported {
1119 statement: "supersede".into(),
1120 message: format!("SUPERSEDE failed: {e}"),
1121 }),
1122 }
1123 }
1124 CalStatement::AddWorkflow(wf) => {
1125 if !self.config.tier1_enabled {
1126 return Err(CalError::Tier1NotEnabled {
1127 statement: "ADD WORKFLOW".into(),
1128 span: wf.span,
1129 });
1130 }
1131 let mut fields = serde_json::Map::new();
1133 fields.insert("name".into(), serde_json::Value::String(wf.name.clone()));
1134 fields.insert(
1136 "nodes".into(),
1137 serde_json::Value::Array(
1138 wf.nodes
1139 .iter()
1140 .map(|n| serde_json::Value::String(n.clone()))
1141 .collect(),
1142 ),
1143 );
1144 let edges_json: Vec<serde_json::Value> = wf
1147 .edges
1148 .iter()
1149 .map(|e| {
1150 let mut m = serde_json::Map::new();
1151 m.insert("src".into(), serde_json::Value::String(e.src.clone()));
1152 m.insert("dst".into(), serde_json::Value::String(e.dst.clone()));
1153 if let Some(ref c) = e.cond {
1154 m.insert("cond".into(), serde_json::Value::String(c.clone()));
1155 }
1156 serde_json::Value::Object(m)
1157 })
1158 .collect();
1159 fields.insert("edges".into(), serde_json::Value::Array(edges_json));
1160 if !wf.bindings.is_empty() {
1162 let mut bind_map = serde_json::Map::new();
1163 for b in &wf.bindings {
1164 bind_map.insert(
1165 b.node.clone(),
1166 serde_json::Value::String(format!("sha256:{}", b.hash)),
1167 );
1168 }
1169 fields.insert("bindings".into(), serde_json::Value::Object(bind_map));
1170 }
1171 let mut retries_map = serde_json::Map::new();
1180 for e in &wf.edges {
1181 if let Some(r) = e.repeat {
1182 let merged = retries_map
1183 .get(&e.dst)
1184 .and_then(|v| v.as_u64())
1185 .map_or(r, |prev| (prev as u32).max(r));
1186 retries_map.insert(
1187 e.dst.clone(),
1188 serde_json::Value::Number(serde_json::Number::from(merged)),
1189 );
1190 }
1191 }
1192 if !retries_map.is_empty() {
1193 fields.insert("retries".into(), serde_json::Value::Object(retries_map));
1194 }
1195 self.inject_write_namespace(&mut fields, store);
1196 let options = build_add_options(&wf.with_options, exec_warnings);
1197 match store.cal_add_with_options("workflow", &fields, options) {
1198 Ok(result) => Ok(CalResultPayload::Added {
1199 hash: hex::encode(result.hash.as_bytes()),
1200 grain_type: "workflow".into(),
1201 extracted_count: None,
1202 extraction_warnings: vec![],
1203 }),
1204 Err(e) => Ok(CalResultPayload::Unsupported {
1205 statement: "add workflow".into(),
1206 message: format!("ADD workflow failed: {e}"),
1207 }),
1208 }
1209 }
1210 CalStatement::SupersedeWorkflow(wf) => {
1211 if !self.config.tier1_enabled {
1212 return Err(CalError::Tier1NotEnabled {
1213 statement: "SUPERSEDE WORKFLOW".into(),
1214 span: wf.span,
1215 });
1216 }
1217 let raw_hash = wf.hash.strip_prefix("sha256:").unwrap_or(&wf.hash);
1218 let old_hash = match Hash::from_hex(raw_hash) {
1219 Ok(h) => h,
1220 Err(e) => {
1221 return Ok(CalResultPayload::Unsupported {
1222 statement: "supersede workflow".into(),
1223 message: format!("invalid hash: {e}"),
1224 })
1225 }
1226 };
1227 let mut fields = serde_json::Map::new();
1229 fields.insert(
1230 "nodes".into(),
1231 serde_json::Value::Array(
1232 wf.nodes
1233 .iter()
1234 .map(|n| serde_json::Value::String(n.clone()))
1235 .collect(),
1236 ),
1237 );
1238 let edges_json: Vec<serde_json::Value> = wf
1239 .edges
1240 .iter()
1241 .map(|e| {
1242 let mut m = serde_json::Map::new();
1243 m.insert("src".into(), serde_json::Value::String(e.src.clone()));
1244 m.insert("dst".into(), serde_json::Value::String(e.dst.clone()));
1245 if let Some(ref c) = e.cond {
1246 m.insert("cond".into(), serde_json::Value::String(c.clone()));
1247 }
1248 serde_json::Value::Object(m)
1249 })
1250 .collect();
1251 fields.insert("edges".into(), serde_json::Value::Array(edges_json));
1252 if !wf.bindings.is_empty() {
1253 let mut bind_map = serde_json::Map::new();
1254 for b in &wf.bindings {
1255 bind_map.insert(
1256 b.node.clone(),
1257 serde_json::Value::String(format!("sha256:{}", b.hash)),
1258 );
1259 }
1260 fields.insert("bindings".into(), serde_json::Value::Object(bind_map));
1261 }
1262 let mut retries_map = serde_json::Map::new();
1269 for e in &wf.edges {
1270 if let Some(r) = e.repeat {
1271 let merged = retries_map
1272 .get(&e.dst)
1273 .and_then(|v| v.as_u64())
1274 .map_or(r, |prev| (prev as u32).max(r));
1275 retries_map.insert(
1276 e.dst.clone(),
1277 serde_json::Value::Number(serde_json::Number::from(merged)),
1278 );
1279 }
1280 }
1281 if !retries_map.is_empty() {
1282 fields.insert("retries".into(), serde_json::Value::Object(retries_map));
1283 }
1284 if !wf.reason.is_empty() {
1285 fields.insert(
1286 "supersede_reason".into(),
1287 serde_json::Value::String(wf.reason.clone()),
1288 );
1289 }
1290 match store.cal_supersede(&old_hash, "workflow", &fields) {
1291 Ok(new_hash) => Ok(CalResultPayload::Superseded {
1292 old_hash: hex::encode(old_hash.as_bytes()),
1293 new_hash: hex::encode(new_hash.as_bytes()),
1294 }),
1295 Err(e) => Ok(CalResultPayload::Unsupported {
1296 statement: "supersede workflow".into(),
1297 message: format!("SUPERSEDE workflow failed: {e}"),
1298 }),
1299 }
1300 }
1301 CalStatement::Accumulate(acc) => {
1302 if !self.config.tier1_enabled {
1303 return Err(CalError::Tier1NotEnabled {
1304 statement: "ACCUMULATE".into(),
1305 span: acc.span,
1306 });
1307 }
1308
1309 let add_ops: Vec<(String, f64)> = acc
1311 .add_ops
1312 .iter()
1313 .map(|op| (op.field.clone(), op.delta))
1314 .collect();
1315
1316 let mut set_map = serde_json::Map::new();
1318 for s in &acc.set_ops {
1319 set_map.insert(s.field.clone(), cal_value_to_json(&s.value));
1320 }
1321 if !acc.reason.is_empty() {
1322 set_map.insert(
1323 "supersede_reason".into(),
1324 serde_json::Value::String(acc.reason.clone()),
1325 );
1326 }
1327
1328 match store.cal_accumulate(
1329 acc.grain_type.as_str(),
1330 &acc.target,
1331 &add_ops,
1332 &set_map,
1333 &acc.reason,
1334 ) {
1335 Ok(result) => Ok(CalResultPayload::Accumulated {
1336 old_hash: hex::encode(result.old_hash.as_bytes()),
1337 new_hash: hex::encode(result.new_hash.as_bytes()),
1338 deltas: result
1339 .applied_deltas
1340 .iter()
1341 .map(|(f, old, new)| AccumulatedDelta {
1342 field: f.clone(),
1343 old_value: *old,
1344 new_value: *new,
1345 })
1346 .collect(),
1347 }),
1348 Err(areev_core::error::AreevError::AccumulateRetryExhausted) => {
1354 tracing::error!(
1358 cal_code = "CAL-E083",
1359 "ACCUMULATE retry budget exhausted under sustained contention"
1360 );
1361 let (subject, relation) = match &acc.target {
1362 super::ast::AccumulateTarget::TipResolved {
1363 subject, relation, ..
1364 } => (subject.clone(), relation.clone()),
1365 super::ast::AccumulateTarget::Hash { .. } => {
1366 (String::new(), String::new())
1367 }
1368 };
1369 Err(CalError::AccumulateRetryExhausted {
1370 subject,
1371 relation,
1372 span: acc.span,
1373 })
1374 }
1375 Err(areev_core::error::AreevError::AccumulateInternal(detail)) => {
1376 tracing::error!(
1377 cal_code = "CAL-E084",
1378 error = %detail,
1379 "ACCUMULATE internal failure"
1380 );
1381 Err(CalError::AccumulateInternal { span: acc.span })
1382 }
1383 Err(areev_core::error::AreevError::AccumulateBackpressureRejected) => {
1388 tracing::warn!(cal_code = "CAL-E085", "ACCUMULATE backpressure rejected");
1389 let (subject, relation) = match &acc.target {
1390 super::ast::AccumulateTarget::TipResolved {
1391 subject, relation, ..
1392 } => (subject.clone(), relation.clone()),
1393 super::ast::AccumulateTarget::Hash { .. } => {
1394 (String::new(), String::new())
1395 }
1396 };
1397 Err(CalError::AccumulateBackpressureRejected {
1398 subject,
1399 relation,
1400 span: acc.span,
1401 })
1402 }
1403 Err(e) => Ok(CalResultPayload::Unsupported {
1409 statement: "accumulate".into(),
1410 message: format!("ACCUMULATE failed: {e}"),
1411 }),
1412 }
1413 }
1414 CalStatement::Revert(_) => Ok(CalResultPayload::Unsupported {
1415 statement: "revert".into(),
1416 message: "Tier 1 REVERT semantics are not yet defined. \
1417 Use EXPLAIN to preview without execution."
1418 .into(),
1419 }),
1420
1421 CalStatement::Forget(forget) => {
1423 if !self.config.allow_destructive_ops {
1424 return Ok(CalResultPayload::Unsupported {
1425 statement: "forget".into(),
1426 message: "Destructive operations are disabled for this session \
1427 (started with --no-destructive-ops)."
1428 .into(),
1429 });
1430 }
1431 match &forget.target {
1432 super::ast::ForgetTarget::Hash { hash } => {
1433 let h = Hash::from_hex(hash).map_err(|_| CalError::InvalidHash {
1434 found: hash.clone(),
1435 span: forget.span,
1436 })?;
1437 match store.cal_delete(&h, forget.reason.as_deref()) {
1438 Ok(()) => Ok(CalResultPayload::Forgotten {
1439 target: format!("hash:{hash}"),
1440 count: 1,
1441 }),
1442 Err(e) => Ok(CalResultPayload::Unsupported {
1443 statement: "forget".into(),
1444 message: format!("FORGET failed: {e}"),
1445 }),
1446 }
1447 }
1448 super::ast::ForgetTarget::User { user_id } => {
1449 let because = match forget.reason.as_deref() {
1453 Some(r) if !r.trim().is_empty() => r,
1454 _ => {
1455 return Err(CalError::MissingReason { span: forget.span });
1456 }
1457 };
1458 match store.cal_forget_user(user_id, forget.text_mentions, because) {
1459 Ok(proof) => Ok(CalResultPayload::Forgotten {
1460 target: format!("subject:{user_id}"),
1461 count: proof.count,
1462 }),
1463 Err(e) => Ok(CalResultPayload::Unsupported {
1464 statement: "forget".into(),
1465 message: format!("FORGET SUBJECT failed: {e}"),
1466 }),
1467 }
1468 }
1469 super::ast::ForgetTarget::Scope { scope } => {
1470 match store.cal_forget_scope(scope) {
1471 Ok(proof) => Ok(CalResultPayload::Forgotten {
1472 target: format!("scope:{scope}"),
1473 count: proof.count,
1474 }),
1475 Err(e) => Ok(CalResultPayload::Unsupported {
1476 statement: "forget".into(),
1477 message: format!("FORGET SCOPE failed: {e}"),
1478 }),
1479 }
1480 }
1481 }
1482 }
1483
1484 CalStatement::DefineTemplate(def) => {
1487 store
1488 .define_template(
1489 &def.name,
1490 &def.source,
1491 def.description.as_deref(),
1492 def.parent.as_deref(),
1493 &def.grain_types,
1494 )
1495 .map_err(|e| {
1496 let s = e.to_string();
1497 if s.contains("AUT-E") {
1500 return CalError::NotAuthorized { detail: s, span: None };
1501 }
1502 if s.contains("CAL-E04")
1506 || s.contains("CAL-E11")
1507 || s.contains("Unknown template")
1508 || s.contains("Invalid template")
1509 || s.contains("syntax")
1510 {
1511 CalError::TemplateSyntaxError {
1512 detail: s,
1513 span: None,
1514 }
1515 } else {
1516 CalError::TemplateInvalidName {
1517 name: def.name.clone(),
1518 span: None,
1519 }
1520 }
1521 })?;
1522 Ok(CalResultPayload::TemplateDefined {
1523 name: def.name.clone(),
1524 })
1525 }
1526 CalStatement::DropTemplate(drop) => {
1527 if !self.config.allow_destructive_ops {
1528 return Ok(CalResultPayload::Unsupported {
1529 statement: "drop_template".into(),
1530 message: "Destructive operations are disabled for this session \
1531 (started with --no-destructive-ops)."
1532 .into(),
1533 });
1534 }
1535 match store.drop_template(&drop.name) {
1536 Ok(()) => Ok(CalResultPayload::TemplateDropped {
1537 name: drop.name.clone(),
1538 }),
1539 Err(e) => Ok(CalResultPayload::Unsupported {
1540 statement: "drop_template".into(),
1541 message: format!("DROP TEMPLATE failed: {e}"),
1542 }),
1543 }
1544 }
1545
1546 CalStatement::DefineQuery(def) => {
1549 store
1550 .define_query(
1551 &def.name,
1552 &def.body,
1553 def.description.as_deref(),
1554 &def.params,
1555 )
1556 .map_err(|e| {
1557 let detail = e.to_string();
1558 if detail.contains("AUT-E") {
1559 CalError::NotAuthorized { detail, span: None }
1560 } else {
1561 CalError::InvalidQueryBody { detail, span: None }
1562 }
1563 })?;
1564 Ok(CalResultPayload::QueryDefined {
1565 name: def.name.clone(),
1566 })
1567 }
1568 CalStatement::DropQuery(drop) => {
1569 if !self.config.allow_destructive_ops {
1570 return Ok(CalResultPayload::Unsupported {
1571 statement: "drop_query".into(),
1572 message: "Destructive operations are disabled for this session \
1573 (started with --no-destructive-ops)."
1574 .into(),
1575 });
1576 }
1577 match store.drop_query(&drop.name) {
1578 Ok(()) => Ok(CalResultPayload::QueryDropped {
1579 name: drop.name.clone(),
1580 }),
1581 Err(e) => Ok(CalResultPayload::Unsupported {
1582 statement: "drop_query".into(),
1583 message: format!("DROP QUERY failed: {e}"),
1584 }),
1585 }
1586 }
1587 CalStatement::RunQuery(run) => self.execute_run_query(run, store, query, exec_warnings),
1588
1589 CalStatement::Purge(purge) => {
1591 if !self.config.allow_destructive_ops {
1592 return Ok(CalResultPayload::Unsupported {
1593 statement: "purge".into(),
1594 message: "Destructive operations are disabled for this session \
1595 (started with --no-destructive-ops)."
1596 .into(),
1597 });
1598 }
1599 let because = match purge.reason.as_deref() {
1602 Some(r) if !r.trim().is_empty() => r,
1603 _ => {
1604 return Err(CalError::MissingReason { span: purge.span });
1605 }
1606 };
1607 let min_age = purge.min_age_days.unwrap_or(30.0);
1608 let batch_limit = purge.limit.unwrap_or(1000);
1609 let ns = purge.namespace.as_deref();
1610 match store.cal_purge_stale(
1611 min_age,
1612 ns,
1613 batch_limit,
1614 purge.grain_type.as_deref(),
1615 because,
1616 ) {
1617 Ok(count) => Ok(CalResultPayload::Purged { count }),
1618 Err(e) => Ok(CalResultPayload::Unsupported {
1619 statement: "purge".into(),
1620 message: format!("PURGE failed: {e}"),
1621 }),
1622 }
1623 }
1624
1625 CalStatement::ReportSubject(rs) => {
1629 match store.cal_subject_report(&rs.subject_id, rs.text_mentions) {
1630 Ok(report) => Ok(CalResultPayload::SubjectReport {
1631 subject: rs.subject_id.clone(),
1632 identity_names: report.identity_names,
1633 grains: report.grains,
1634 }),
1635 Err(e) => Ok(CalResultPayload::Unsupported {
1636 statement: "report_subject".into(),
1637 message: format!("REPORT SUBJECT failed: {e}"),
1638 }),
1639 }
1640 }
1641
1642 CalStatement::EntityAt(ea) => {
1646 let axis = ea.axis.clone().unwrap_or_else(|| "world".to_string());
1647 match store.cal_entity_at(&ea.subject, &ea.relation, ea.at_ms, &axis) {
1648 Ok(grain) => Ok(CalResultPayload::EntityAt { grain, axis, at_ms: ea.at_ms }),
1649 Err(e) => Ok(CalResultPayload::Unsupported {
1650 statement: "entity_at".into(),
1651 message: format!("ENTITY AT failed: {e}"),
1652 }),
1653 }
1654 }
1655 CalStatement::RunTrace(rt) => {
1656 match store.cal_run_trace(&rt.run_id, rt.limit.unwrap_or(64).min(1024)) {
1657 Ok(trace) => Ok(CalResultPayload::RunTrace {
1658 run_id: rt.run_id.clone(),
1659 trace,
1660 }),
1661 Err(e) => Ok(CalResultPayload::Unsupported {
1662 statement: "run_trace".into(),
1663 message: format!("RUN TRACE failed: {e}"),
1664 }),
1665 }
1666 }
1667 CalStatement::RunsTouching(rt) => {
1668 let h = Hash::from_hex(&rt.hash).map_err(|_| CalError::InvalidHash {
1669 found: rt.hash.clone(),
1670 span: rt.span,
1671 })?;
1672 match store.cal_runs_touching(&h, rt.depth.unwrap_or(4).min(8)) {
1673 Ok(runs) => Ok(CalResultPayload::RunsTouching { hash: rt.hash.clone(), runs }),
1674 Err(e) => Ok(CalResultPayload::Unsupported {
1675 statement: "runs_touching".into(),
1676 message: format!("RUNS TOUCHING failed: {e}"),
1677 }),
1678 }
1679 }
1680 CalStatement::DerivedFrom(df) => {
1681 let h = Hash::from_hex(&df.hash).map_err(|_| CalError::InvalidHash {
1682 found: df.hash.clone(),
1683 span: df.span,
1684 })?;
1685 match store.cal_derived_from(&h) {
1686 Ok(grains) => Ok(CalResultPayload::DerivedFrom { hash: df.hash.clone(), grains }),
1687 Err(e) => Ok(CalResultPayload::Unsupported {
1688 statement: "derived_from".into(),
1689 message: format!("DERIVED FROM failed: {e}"),
1690 }),
1691 }
1692 }
1693 CalStatement::Merge(mg) => {
1694 if !self.config.tier1_enabled {
1695 return Err(CalError::Tier1NotEnabled {
1696 statement: "MERGE".into(),
1697 span: mg.span,
1698 });
1699 }
1700 match store.cal_merge(
1701 &mg.subject,
1702 &mg.relation,
1703 &mg.object,
1704 mg.confidence.unwrap_or(0.9),
1705 &mg.reason,
1706 ) {
1707 Ok(hash) => Ok(CalResultPayload::Merged {
1708 hash: hash.to_hex(),
1709 subject: mg.subject.clone(),
1710 relation: mg.relation.clone(),
1711 object: mg.object.clone(),
1712 }),
1713 Err(e) => Ok(CalResultPayload::Unsupported {
1714 statement: "merge".into(),
1715 message: format!("MERGE failed: {e}"),
1716 }),
1717 }
1718 }
1719 CalStatement::Related(rel) => {
1720 let relations: Vec<&str> = rel
1721 .relations
1722 .split(',')
1723 .map(str::trim)
1724 .filter(|r| !r.is_empty())
1725 .collect();
1726 match store.cal_related(
1727 &rel.start,
1728 &relations,
1729 rel.direction.as_deref().unwrap_or("out"),
1730 rel.depth.unwrap_or(2),
1731 rel.limit.unwrap_or(64),
1732 ) {
1733 Ok(entities) => Ok(CalResultPayload::RelatedEntities {
1734 start: rel.start.clone(),
1735 entities,
1736 }),
1737 Err(e) => Ok(CalResultPayload::Unsupported {
1738 statement: "related".into(),
1739 message: format!("RELATED failed: {e}"),
1740 }),
1741 }
1742 }
1743 CalStatement::Novelty(nv) => {
1744 match store.cal_novelty(
1745 &nv.text,
1746 nv.subject.as_deref(),
1747 nv.relation.as_deref(),
1748 nv.limit.unwrap_or(5),
1749 ) {
1750 Ok(rows) => Ok(CalResultPayload::NoveltyMatches {
1751 matches: rows
1752 .into_iter()
1753 .map(|(hash, similarity)| {
1754 serde_json::json!({ "hash": hash, "similarity": similarity })
1755 })
1756 .collect(),
1757 }),
1758 Err(e) => Ok(CalResultPayload::Unsupported {
1759 statement: "novelty".into(),
1760 message: format!("NOVELTY failed: {e}"),
1761 }),
1762 }
1763 }
1764 CalStatement::ShowForks(_) => match store.open_forks() {
1765 Ok(groups) => Ok(CalResultPayload::Forks {
1766 forks: groups
1767 .iter()
1768 .map(|f| {
1769 serde_json::json!({
1770 "namespace": f.namespace,
1771 "subject": f.subject,
1772 "relation": f.relation,
1773 "heads": f.heads,
1774 })
1775 })
1776 .collect(),
1777 }),
1778 Err(e) => Ok(CalResultPayload::Unsupported {
1779 statement: "show_forks".into(),
1780 message: format!("SHOW FORKS failed: {e}"),
1781 }),
1782 },
1783
1784 CalStatement::Remember(rem) => {
1786 if !self.config.tier1_enabled {
1787 return Err(CalError::Tier1NotEnabled {
1788 statement: "REMEMBER".into(),
1789 span: rem.span,
1790 });
1791 }
1792 match store.cal_remember(
1793 &rem.content,
1794 rem.session_id.as_deref(),
1795 rem.role.as_deref(),
1796 rem.run_id.as_deref(),
1797 ) {
1798 Ok(hash) => Ok(CalResultPayload::Remembered { hash: hash.to_hex() }),
1799 Err(e) => Ok(CalResultPayload::Unsupported {
1800 statement: "remember".into(),
1801 message: format!("REMEMBER failed: {e}"),
1802 }),
1803 }
1804 }
1805
1806 CalStatement::Grant(grant) => {
1811 if !self.config.tier1_enabled {
1812 return Err(CalError::Tier1NotEnabled {
1813 statement: "GRANT".into(),
1814 span: grant.span,
1815 });
1816 }
1817 match store.cal_grant(
1818 &grant.principal,
1819 &grant.verbs,
1820 &grant.namespaces,
1821 grant.reason.as_deref(),
1822 ) {
1823 Ok(hash) => {
1824 let object = areev_core::authz::Grant {
1825 verbs: grant
1826 .verbs
1827 .iter()
1828 .filter_map(|v| areev_core::authz::Verb::parse(v).ok())
1829 .collect(),
1830 namespaces: grant.namespaces.clone(),
1831 }
1832 .to_object_string();
1833 Ok(CalResultPayload::Granted {
1834 principal: grant.principal.clone(),
1835 object,
1836 hash: hash.to_hex(),
1837 })
1838 }
1839 Err(e) => Ok(CalResultPayload::Unsupported {
1840 statement: "grant".into(),
1841 message: format!("GRANT failed: {e}"),
1842 }),
1843 }
1844 }
1845 CalStatement::Revoke(revoke) => {
1846 if !self.config.tier1_enabled {
1847 return Err(CalError::Tier1NotEnabled {
1848 statement: "REVOKE".into(),
1849 span: revoke.span,
1850 });
1851 }
1852 match store.cal_revoke(
1853 &revoke.principal,
1854 &revoke.verbs,
1855 &revoke.namespaces,
1856 revoke.reason.as_deref(),
1857 ) {
1858 Ok(grants_touched) => Ok(CalResultPayload::Revoked {
1859 principal: revoke.principal.clone(),
1860 grants_touched,
1861 }),
1862 Err(e) => Ok(CalResultPayload::Unsupported {
1863 statement: "revoke".into(),
1864 message: format!("REVOKE failed: {e}"),
1865 }),
1866 }
1867 }
1868 CalStatement::Approve(g) | CalStatement::Reject(g) => {
1873 let decision = if matches!(stmt, CalStatement::Approve(_)) {
1874 crate::governance::ReviewDecision::Approve
1875 } else {
1876 crate::governance::ReviewDecision::Reject
1877 };
1878 let name = if decision == crate::governance::ReviewDecision::Approve {
1879 "approve"
1880 } else {
1881 "reject"
1882 };
1883 let Some(host) = &self.governance else {
1884 return Ok(governance_unwired(name));
1885 };
1886 match host.review(store, &g.hash, decision, &g.reason) {
1887 Ok(()) => Ok(CalResultPayload::Reviewed {
1888 hash: g.hash.clone(),
1889 decision: name.into(),
1890 }),
1891 Err(e) => Ok(CalResultPayload::Unsupported {
1892 statement: name.into(),
1893 message: format!("{} failed: {e}", name.to_uppercase()),
1894 }),
1895 }
1896 }
1897 CalStatement::ApplyRec(g) => {
1898 let Some(host) = &self.governance else {
1899 return Ok(governance_unwired("apply"));
1900 };
1901 match host.apply(
1902 store,
1903 &g.hash,
1904 &g.reason,
1905 self.config.allow_destructive_ops,
1906 ) {
1907 Ok(rollbackable) => Ok(CalResultPayload::RecApplied {
1908 hash: g.hash.clone(),
1909 rollbackable,
1910 }),
1911 Err(e) => Ok(CalResultPayload::Unsupported {
1912 statement: "apply".into(),
1913 message: format!("APPLY failed: {e}"),
1914 }),
1915 }
1916 }
1917 CalStatement::RollbackRec(g) => {
1918 let Some(host) = &self.governance else {
1919 return Ok(governance_unwired("rollback"));
1920 };
1921 match host.rollback(store, &g.hash, &g.reason) {
1922 Ok(()) => Ok(CalResultPayload::RecRolledBack { hash: g.hash.clone() }),
1923 Err(e) => Ok(CalResultPayload::Unsupported {
1924 statement: "rollback".into(),
1925 message: format!("ROLLBACK failed: {e}"),
1926 }),
1927 }
1928 }
1929 CalStatement::RunLoop(run) => {
1930 let Some(host) = &self.governance else {
1931 return Ok(governance_unwired("run_loop"));
1932 };
1933 let opts = crate::governance::RunLoopOptions {
1934 full_sweep: run.full_sweep,
1935 min_new: run.min_new,
1936 if_stale_ms: run.if_stale_ms,
1937 };
1938 match host.run_loop(store, &opts) {
1939 Ok(report) => Ok(CalResultPayload::LoopRan { run: report }),
1940 Err(e) => Ok(CalResultPayload::Unsupported {
1941 statement: "run_loop".into(),
1942 message: format!("RUN LOOP failed: {e}"),
1943 }),
1944 }
1945 }
1946 CalStatement::ShowGrants(show) => {
1947 match store.cal_show_grants(show.principal.as_deref()) {
1948 Ok(rows) => Ok(CalResultPayload::GrantList {
1949 grants: rows
1950 .into_iter()
1951 .map(|row| {
1952 let parsed =
1953 areev_core::authz::Grant::from_object_string(&row.object)
1954 .ok();
1955 serde_json::json!({
1956 "principal": row.principal,
1957 "object": row.object,
1958 "verbs": parsed.as_ref().map(|g| g
1959 .verbs
1960 .iter()
1961 .map(|v| v.as_str())
1962 .collect::<Vec<_>>()),
1963 "namespaces": parsed.as_ref().map(|g| g.namespaces.clone()),
1964 "hash": row.hash,
1965 })
1966 })
1967 .collect(),
1968 }),
1969 Err(e) => Ok(CalResultPayload::Unsupported {
1970 statement: "show_grants".into(),
1971 message: format!("SHOW GRANTS failed: {e}"),
1972 }),
1973 }
1974 }
1975 }
1976 }
1977
1978 fn execute_run_query(
1983 &self,
1984 run: &super::ast::RunQueryStmt,
1985 store: &dyn CalStoreFacade,
1986 outer_query: &CalQuery,
1987 exec_warnings: &mut Vec<String>,
1988 ) -> std::result::Result<CalResultPayload, CalError> {
1989 let entry = store
1991 .get_query(&run.name)
1992 .ok_or_else(|| CalError::QueryNotFound {
1993 name: run.name.clone(),
1994 span: run.span,
1995 })?;
1996
1997 let mut body = entry.body.clone();
1999
2000 let mut param_values: HashMap<String, String> = HashMap::new();
2002
2003 for p in &entry.params {
2005 if let Some(ref default) = p.default {
2006 param_values.insert(p.name.clone(), value_to_cal_literal(default));
2007 }
2008 }
2009
2010 for (name, value) in &run.bindings {
2012 param_values.insert(name.clone(), value_to_cal_literal(value));
2013 }
2014
2015 for p in &entry.params {
2017 if p.default.is_none() && !param_values.contains_key(&p.name) {
2018 return Err(CalError::MissingQueryParam {
2019 name: p.name.clone(),
2020 query: run.name.clone(),
2021 span: run.span,
2022 });
2023 }
2024 }
2025
2026 let declared_names: std::collections::HashSet<&str> =
2028 entry.params.iter().map(|p| p.name.as_str()).collect();
2029 for (name, _) in &run.bindings {
2030 if !declared_names.contains(name.as_str()) {
2031 exec_warnings.push(format!(
2032 "CAL-W006: Parameter \"${}\" supplied but not used in query \"{}\"",
2033 name, run.name
2034 ));
2035 }
2036 }
2037
2038 for (name, literal) in ¶m_values {
2040 body = body.replace(&format!("${}", name), literal);
2041 }
2042
2043 let parsed = self.parse_cached(&body).map_err(|e| CalError::InvalidQueryBody {
2054 detail: e.to_string(),
2055 span: run.span,
2056 })?;
2057
2058 let span = run.span.unwrap_or_else(Span::zero);
2063 super::parser::check_read_only_statement(&parsed.statement, &span)?;
2064
2065 let mut merged_query = parsed;
2067
2068 for opt in &outer_query.with_options {
2070 let existing_idx = merged_query
2072 .with_options
2073 .iter()
2074 .position(|o| std::mem::discriminant(o) == std::mem::discriminant(opt));
2075 if let Some(idx) = existing_idx {
2076 merged_query.with_options[idx] = opt.clone();
2078 } else {
2079 merged_query.with_options.push(opt.clone());
2080 }
2081 }
2082
2083 if outer_query.format.is_some() {
2085 merged_query.format = outer_query.format.clone();
2086 }
2087
2088 for stage in &outer_query.pipeline {
2090 merged_query.pipeline.push(stage.clone());
2091 }
2092
2093 for (k, v) in &outer_query.user_vars {
2095 merged_query
2096 .user_vars
2097 .entry(k.clone())
2098 .or_insert_with(|| v.clone());
2099 }
2100
2101 let result =
2103 self.execute_statement(&merged_query.statement, store, &merged_query, exec_warnings)?;
2104
2105 let _ = store.update_query_last_run(&run.name);
2108
2109 Ok(result)
2110 }
2111
2112 fn execute_recall(
2117 &self,
2118 recall: &RecallStmt,
2119 store: &dyn CalStoreFacade,
2120 query: &CalQuery,
2121 exec_warnings: &mut Vec<String>,
2122 ) -> std::result::Result<CalResultPayload, CalError> {
2123 let mut params = RecallParams::default();
2124
2125 if let Some(gt) = recall.grain_type.to_grain_type() {
2127 params.grain_type = Some(gt);
2128 }
2129
2130 if let Some(ref about) = recall.about {
2132 params.query = Some(about.text.clone());
2133 }
2134
2135 if params.query.is_none() {
2138 if let Some(ref like) = recall.like {
2139 params.query = Some(like.text.clone());
2140 }
2141 }
2142
2143 if let Some(ref where_clause) = recall.where_clause {
2145 self.apply_where_clause(&where_clause.condition, &mut params, exec_warnings, &query.let_values)?;
2146 }
2147
2148 if recall.grain_type == GrainTypePlural::Consents {
2152 let expand_did = |value: &str| -> Vec<String> {
2153 let mut variants = vec![value.to_string()];
2154 if !value.starts_with("did:") {
2155 variants.push(format!("did:{}", value));
2156 }
2157 variants
2158 };
2159
2160 if let Some(ref subj) = params.subject.take() {
2161 let expanded = expand_did(subj);
2162 match params.subject_in.as_mut() {
2163 Some(existing) => existing.extend(expanded),
2164 None => params.subject_in = Some(expanded),
2165 }
2166 }
2167 if let Some(ref existing) = params.subject_in.clone() {
2168 let mut seen = std::collections::HashSet::new();
2170 let deduped: Vec<String> = existing
2171 .iter()
2172 .flat_map(|s| expand_did(s))
2173 .filter(|s| seen.insert(s.clone()))
2174 .collect();
2175 params.subject_in = Some(deduped);
2176 }
2177 }
2178
2179 match (&recall.since, &recall.until) {
2181 (Some(since), Some(until)) => {
2182 params.temporal_expr = Some(format!(
2184 "between {} and {}",
2185 since.expression, until.expression
2186 ));
2187 }
2188 (Some(since), None) => {
2189 params.temporal_expr = Some(since.expression.clone());
2190 }
2191 (None, Some(until)) => {
2192 params.temporal_expr = Some(format!("before {}", until.expression));
2193 }
2194 (None, None) => {}
2195 }
2196
2197 if let Some(ref between) = recall.between {
2199 params.temporal_expr = Some(format!("between {} and {}", between.start, between.end));
2200 }
2201
2202 if let Some(ref recent) = recall.recent {
2204 params.limit = Some(recent.count.min(self.config.max_limit) as usize);
2205 }
2206
2207 if let Some(limit) = recall.limit {
2209 params.limit = Some(limit.min(self.config.max_limit) as usize);
2210 }
2211
2212 if params.limit.is_none() {
2214 params.limit = Some(self.config.default_limit as usize);
2215 }
2216
2217 let contradictions_limit = if recall.contradictions.is_some() {
2227 params.limit.replace(self.config.max_limit as usize)
2228 } else {
2229 None
2230 };
2231
2232 let order_by: Option<(String, bool)> = query.pipeline.iter().find_map(|st| match st {
2255 PipelineStage::OrderBy {
2256 field, descending, ..
2257 } => Some((field.clone(), *descending)),
2258 _ => None,
2259 });
2260 let has_count = query
2261 .pipeline
2262 .iter()
2263 .any(|st| matches!(st, PipelineStage::Count { .. }));
2264 let residual_where = match recall.where_clause.as_ref() {
2269 Some(w) => plan_residual_where(&w.condition, &recall.grain_type, exec_warnings)?,
2270 None => None,
2271 };
2272 let has_post_filter = residual_where.is_some();
2273 let wide_reason: Option<String> = if recall.contradictions.is_some() {
2275 None
2276 } else if let Some((ref f, _)) = order_by {
2277 Some(format!("ORDER BY {f}"))
2278 } else if has_count {
2279 Some("COUNT".to_string())
2280 } else if has_post_filter {
2281 Some("a post-retrieval WHERE filter".to_string())
2282 } else {
2283 None
2284 };
2285 let pushed_down_sort = matches!(order_by, Some((ref f, _)) if f == "created_at");
2288 if let (true, Some((ref field, descending))) = (pushed_down_sort, &order_by) {
2289 params.order_by = Some(crate::store_types::SortKey {
2290 field: field.clone(),
2291 descending: *descending,
2292 });
2293 }
2294 let widened_limit = if wide_reason.is_some() && !pushed_down_sort {
2295 params.limit.replace(self.config.max_limit as usize)
2296 } else {
2297 None
2298 };
2299
2300 self.apply_with_options(&query.with_options, &mut params)?;
2302
2303 if params.exhaustive.is_some() && recall.about.is_none() {
2305 return Err(CalError::UnexpectedToken {
2306 expected: "ABOUT clause (WITH exhaustive requires a semantic search query)".into(),
2307 found: "no ABOUT clause".into(),
2308 span: recall.span,
2309 suggestion: Some(
2310 "Add an ABOUT clause: RECALL facts ABOUT \"...\" WITH exhaustive".into(),
2311 ),
2312 });
2313 }
2314
2315 if let Some(ref ns) = self.config.namespace_override {
2317 params.namespace = Some(ns.clone());
2318 params.namespaces = None;
2322 } else if params.namespace.is_none() && params.namespaces.is_none() {
2323 if let Some(ns) = store.default_namespace() {
2326 params.namespace = Some(ns.to_string());
2327 }
2328 }
2329
2330 if let Some(ref uid) = self.config.user_id_override {
2331 params.user_id = Some(uid.clone());
2332 }
2333
2334 let hits = store
2336 .recall(¶ms)
2337 .map_err(|e| map_store_err(e, recall.span))?;
2338
2339 let scan_was_bounded = params.limit.is_some_and(|l| hits.len() >= l);
2342
2343 let mut grains = hits_to_grain_results(&hits);
2344
2345 if recall.about.is_none() {
2348 for g in &mut grains {
2349 g.is_deterministic = true;
2350 }
2351 }
2352
2353 if let Some(ref residual) = residual_where {
2362 grains.retain(|grain| grain_matches_condition_tree(grain, residual));
2363 }
2364
2365 if let Some(reason) = wide_reason {
2374 if scan_was_bounded {
2379 exec_warnings.push(
2380 super::errors::CalWarning::ScanBounded {
2381 stage: reason,
2382 scanned: self.config.max_limit as usize,
2383 }
2384 .to_string(),
2385 );
2386 }
2387 if let Some((ref field, descending)) = order_by {
2388 grains.sort_by(|a, b| {
2389 let cmp = compare_json_values(
2390 json_field(&a.fields, field),
2391 json_field(&b.fields, field),
2392 );
2393 if descending {
2394 cmp.reverse()
2395 } else {
2396 cmp
2397 }
2398 });
2399 }
2400 let pipeline_bounds = query.pipeline.iter().any(|st| {
2404 matches!(
2405 st,
2406 PipelineStage::Limit { .. }
2407 | PipelineStage::First { .. }
2408 | PipelineStage::Count { .. }
2409 )
2410 });
2411 if !pipeline_bounds {
2412 if let Some(limit) = widened_limit {
2413 grains.truncate(limit);
2414 }
2415 }
2416 }
2417
2418 for opt in &query.with_options {
2427 if let WithOption::Dedup { field: Some(field) } = opt {
2430 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
2431 grains.retain(|g| match json_field(&g.fields, field) {
2432 Some(v) => seen.insert(value_dedup_key(v)),
2433 None => true,
2437 });
2438 }
2439 }
2440
2441 if params.score_breakdown == Some(true) {
2449 exec_warnings.push(
2450 super::errors::CalWarning::WithOptionInert {
2451 option: "score_breakdown",
2452 statement: "RECALL",
2453 why: "this recall path returns fused grains, not per-leg scores, so \
2454 structural hits all carry the sentinel score 1.0",
2455 }
2456 .to_string(),
2457 );
2458 }
2459
2460 if let Some(ref clause) = recall.contradictions {
2473 let scope = match clause.inner {
2478 Some(ref inner) => {
2479 let inner_query = CalQuery {
2480 statement: (**inner).clone(),
2481 pipeline: Vec::new(),
2482 format: None,
2483 ..query.clone()
2484 };
2485 let payload = self.execute_statement(
2486 &inner_query.statement,
2487 store,
2488 &inner_query,
2489 exec_warnings,
2490 )?;
2491 match payload {
2492 CalResultPayload::Grains { ref grains, .. } => Some(
2493 contradiction_scope_keys(grains, params.namespace.as_deref()),
2494 ),
2495 _ => {
2500 exec_warnings.push(
2501 "CONTRADICTIONS OF (...) sub-query returned no grains; \
2502 no keys to scope by"
2503 .into(),
2504 );
2505 Some(std::collections::HashSet::new())
2506 }
2507 }
2508 }
2509 None => None,
2510 };
2511 apply_fork_status(&mut grains, store, true, scope.as_ref(), exec_warnings);
2512
2513 if scan_was_bounded {
2517 exec_warnings.push(
2518 super::errors::CalWarning::ContradictionScanBounded {
2519 scanned: self.config.max_limit as usize,
2520 }
2521 .to_string(),
2522 );
2523 }
2524
2525 if let Some(limit) = contradictions_limit {
2528 grains.truncate(limit);
2529 }
2530 } else if params.detect_contradictions == Some(true) {
2531 apply_fork_status(&mut grains, store, false, None, exec_warnings);
2534 }
2535
2536 let count = grains.len();
2537
2538 if query.pipeline.is_empty() {
2543 if let Some(ref fmt) = query.format {
2544 return apply_format_clause_to_grains(
2545 &grains,
2546 fmt,
2547 None,
2548 RenderInputs {
2549 user_vars: &query.user_vars,
2550 store,
2551 disclosure: disclosure_of(&query.with_options),
2552 },
2553 &Default::default(),
2554 exec_warnings,
2555 );
2556 }
2557 }
2558
2559 Ok(CalResultPayload::Grains {
2560 grains,
2561 total_available: Some(count),
2562 })
2563 }
2564
2565 fn apply_where_clause(
2570 &self,
2571 condition: &Condition,
2572 params: &mut RecallParams,
2573 warnings: &mut Vec<String>,
2574 let_values: &HashMap<String, Vec<String>>,
2578 ) -> std::result::Result<(), CalError> {
2579 match condition {
2580 Condition::Comparison {
2581 field,
2582 comparator,
2583 value,
2584 ..
2585 } => {
2586 match (field.as_str(), comparator) {
2587 ("subject", Comparator::Eq) => {
2588 params.subject = Some(value_to_string(value)?);
2589 }
2590 ("relation", Comparator::Eq) => {
2591 params.relation = Some(value_to_string(value)?);
2592 }
2593 ("object", Comparator::Eq) => {
2594 params.object = Some(value_to_string(value)?);
2595 }
2596 ("namespace", Comparator::Eq) => {
2597 if self.config.namespace_override.is_none() {
2599 params.namespace = Some(value_to_string(value)?);
2600 }
2601 }
2602 ("user_id", Comparator::Eq) => {
2603 if self.config.user_id_override.is_none() {
2605 params.user_id = Some(value_to_string(value)?);
2606 }
2607 }
2608 ("confidence", Comparator::Gte) | ("confidence", Comparator::Gt) => {
2609 params.confidence_threshold = Some(value_to_f64(value)?);
2610 }
2611 ("importance", Comparator::Gte) | ("importance", Comparator::Gt) => {
2612 params.importance_threshold = Some(value_to_f64(value)?);
2613 }
2614 ("query", Comparator::Eq) => {
2615 params.query = Some(value_to_string(value)?);
2616 }
2617 ("session_id", Comparator::Eq) => {
2624 params.session_id = Some(value_to_string(value)?);
2625 }
2626 ("time", Comparator::Eq) => {
2627 params.temporal_expr = Some(value_to_string(value)?);
2628 }
2629 ("contradicted", Comparator::Eq) => {
2630 if let Value::Boolean { value: b } = value {
2631 params.include_contradicted = Some(*b);
2632 }
2633 }
2634 ("entity", Comparator::Eq) => {
2635 params.entity = Some(value_to_string(value)?);
2636 }
2637 ("hash", Comparator::Eq) | ("hash", Comparator::NotEq) => {}
2645 ("scope_path", Comparator::Eq) | ("scope", Comparator::Eq) => {
2646 params.scope_path = Some(value_to_string(value)?);
2647 }
2648 _ => {}
2655 }
2656 Ok(())
2657 }
2658
2659 Condition::And { left, right, .. } => {
2660 self.apply_where_clause(left, params, warnings, let_values)?;
2661 self.apply_where_clause(right, params, warnings, let_values)?;
2662 Ok(())
2663 }
2664
2665 Condition::In { field, values, span } => {
2666 let mut str_values: Vec<String> = Vec::new();
2675 for v in values {
2676 match v {
2677 Value::String { value } | Value::Hash { value } => {
2678 str_values.push(value.clone())
2679 }
2680 Value::Number { value } => str_values.push(value.to_string()),
2681 Value::Parameter { name } => match let_values.get(name.as_str()) {
2682 Some(bound) => str_values.extend(bound.iter().cloned()),
2683 None => {
2684 return Err(CalError::UnboundParameter {
2685 name: name.clone(),
2686 span: *span,
2687 })
2688 }
2689 },
2690 Value::Array { values: inner } => {
2691 for iv in inner {
2692 if let Value::String { value } = iv {
2693 str_values.push(value.clone());
2694 }
2695 }
2696 }
2697 Value::Boolean { value } => str_values.push(value.to_string()),
2698 }
2699 }
2700 match field.as_str() {
2701 "subject" => params.subject_in = Some(str_values),
2702 "relation" => params.relation_in = Some(str_values),
2703 "object" => params.object_in = Some(str_values),
2704 "tags" => params.tags = Some(str_values),
2705 "namespace" => {
2706 params.namespaces = Some(str_values);
2712 }
2713 _ => {
2714 }
2717 }
2718 Ok(())
2719 }
2720
2721 Condition::NotIn { field, values, .. } => {
2722 if field == "tags" {
2723 let tag_strs: Vec<String> = values
2724 .iter()
2725 .filter_map(|v| match v {
2726 Value::String { value } => Some(value.clone()),
2727 _ => None,
2728 })
2729 .collect();
2730 params.exclude_tags = Some(tag_strs);
2731 }
2732 Ok(())
2733 }
2734
2735 Condition::Contains { field, value, .. } => {
2736 match field.as_str() {
2739 "subject" => {
2740 params.subject_contains = Some(value.clone());
2741 }
2742 "object" => {
2743 params.object_contains = Some(value.clone());
2744 }
2745 "content" | "summary" if params.query.is_none() => {
2746 params.query = Some(value.clone());
2747 }
2748 _ => {}
2749 }
2750 Ok(())
2751 }
2752
2753 Condition::Or { .. } => {
2754 Ok(())
2761 }
2762
2763 Condition::IsCategory {
2765 field, category, ..
2766 } => {
2767 if field == "relation" {
2768 let relations = super::relations::expand_category(category);
2769 if relations.is_empty() {
2770 warnings.push(format!(
2771 "Unknown relation category '{}'; no relations expanded.",
2772 category
2773 ));
2774 } else {
2775 params.relation_in =
2778 Some(relations.iter().map(|r| r.to_string()).collect());
2779 }
2780 } else {
2781 warnings.push(format!(
2782 "CAL-W008: IS {} used on field '{}' — IS CATEGORY is only meaningful on the 'relation' field; this condition was ignored.",
2783 category, field
2784 ));
2785 }
2786 Ok(())
2787 }
2788
2789 _ => Ok(()),
2793 }
2794 }
2795
2796 fn apply_with_options(
2801 &self,
2802 options: &[WithOption],
2803 params: &mut RecallParams,
2804 ) -> std::result::Result<(), CalError> {
2805 for opt in options {
2806 match opt {
2807 WithOption::Superseded => {
2808 params.exclude_superseded = Some(false);
2809 }
2810 WithOption::ScoreBreakdown => {
2811 params.score_breakdown = Some(true);
2812 }
2813 WithOption::Explanation => {
2814 params.explanation = Some(true);
2815 }
2816 WithOption::ContradictionDetection => {
2817 params.detect_contradictions = Some(true);
2818 }
2819 WithOption::Diversity { lambda } => {
2820 params.diversity = Some(if let Some(l) = lambda {
2821 DiversityConfig::mmr_with_lambda(*l as f32)
2822 } else {
2823 DiversityConfig::mmr()
2824 });
2825 }
2826
2827 WithOption::Provenance => {
2829 params.record_provenance = Some(true);
2830 }
2831 WithOption::Dedup { field: _ } => {
2832 params.deduplicate = Some(true);
2837 }
2838 WithOption::Rerank { ref model } => {
2843 let mut cfg = crate::store_types::RerankConfig::default();
2844 if let Some(m) = model {
2845 cfg.model = Some(m.clone());
2846 }
2847 params.rerank = Some(cfg);
2848 }
2849 WithOption::LlmRerank { .. } => {
2853 return Err(CalError::LlmFeatureUnavailable { feature: "llm_rerank".into() });
2854 }
2855 WithOption::Hyde => {
2856 return Err(CalError::LlmFeatureUnavailable { feature: "hyde".into() });
2857 }
2858 WithOption::QueryExpansion => {
2859 params.query_expansion = Some(true);
2860 }
2861 WithOption::QueryDecompose => {
2862 params.query_decompose = Some(true);
2863 }
2864 WithOption::ConflictResolution => {
2865 params.conflict_resolution = Some(true);
2866 }
2867 WithOption::IncludeSources => {
2868 params.include_sources = Some(true);
2869 }
2870 WithOption::AnnotateRelativeTime => {
2871 params.annotate_relative_time = Some(true);
2872 }
2873 WithOption::RecencyWeight { weight } => {
2874 params.recency_weight = Some(*weight);
2875 }
2876 WithOption::MinScore { score } => {
2877 params.min_score = Some(*score);
2878 }
2879 WithOption::MultiHop { hops } => {
2880 params.multi_hop = Some((*hops as u8).clamp(1, 3));
2881 }
2882 WithOption::SessionAffinity { boost } => {
2883 params.session_affinity_boost = Some(boost.clamp(0.0, 1.0));
2884 }
2885 WithOption::SubjectAffinity { boost } => {
2886 params.subject_affinity_boost = Some(boost.clamp(0.0, 1.0));
2887 }
2888 WithOption::SessionCoverage { min_per_ns } => {
2889 params.min_per_namespace = Some((*min_per_ns as usize).clamp(1, 10));
2890 }
2891 WithOption::MaxNamespaces { max } => {
2892 params.max_namespaces = Some((*max as usize).clamp(1, 100));
2893 }
2894 WithOption::Exhaustive { max_rounds } => {
2895 let mut config = crate::store_types::ExhaustiveConfig::default();
2896 if let Some(rounds) = max_rounds {
2897 config.max_rounds = (*rounds as u8).clamp(1, 5);
2898 }
2899 config.validate();
2900 params.exhaustive = Some(config);
2901 }
2902 WithOption::SessionCensus {
2903 min_per_session,
2904 min_score,
2905 } => {
2906 let mut config = crate::store_types::SessionCensusConfig::default();
2907 if let Some(mps) = min_per_session {
2908 config.min_per_session = (*mps as u8).clamp(1, 10);
2909 }
2910 if let Some(ms) = min_score {
2911 config.min_score = ms.clamp(0.0, 1.0);
2912 }
2913 config.validate();
2914 params.session_census = Some(config);
2915 }
2916 WithOption::AggregationIntent => {
2917 params.aggregation_intent = Some(true);
2918 }
2919
2920 WithOption::PreferenceEnrichment => {
2921 params.preference_enrichment = Some(true);
2922 }
2923
2924 WithOption::ProgressiveDisclosure { .. } => {}
2931 WithOption::Consistency { .. } => {}
2932 WithOption::Locale { .. } => {}
2933 WithOption::Cache { .. } => {}
2934
2935 }
2936 }
2937 Ok(())
2938 }
2939
2940 #[allow(unused_variables)] #[allow(clippy::ptr_arg)] fn apply_assemble_post_merge_options(
2971 &self,
2972 payload: CalResultPayload,
2973 options: &[WithOption],
2974 warnings: &mut Vec<String>,
2975 store: &dyn CalStoreFacade,
2976 about_text: &str,
2977 ) -> std::result::Result<CalResultPayload, CalError> {
2978 let (mut grains, sources, total_tokens, budget_limit, _total_available) = match payload {
2980 CalResultPayload::Assembled {
2981 grains,
2982 sources,
2983 total_tokens,
2984 budget_limit,
2985 total_available,
2986 ..
2987 } => (grains, sources, total_tokens, budget_limit, total_available),
2988 other => return Ok(other),
2989 };
2990
2991 for opt in options {
2992 match opt {
2993 WithOption::ConflictResolution => {
2998 #[allow(clippy::type_complexity)]
2999 let mut groups: HashMap<
3000 (String, String),
3001 Vec<(i64, usize, String)>,
3002 > = HashMap::new();
3003 for (idx, grain) in grains.iter().enumerate() {
3004 let subj = grain
3005 .fields
3006 .get("subject")
3007 .and_then(|v| v.as_str())
3008 .unwrap_or("")
3009 .to_string();
3010 let rel = grain
3011 .fields
3012 .get("relation")
3013 .and_then(|v| v.as_str())
3014 .unwrap_or("")
3015 .to_string();
3016 if subj.is_empty() && rel.is_empty() {
3017 continue;
3018 }
3019 let obj = grain
3020 .fields
3021 .get("object")
3022 .and_then(|v| v.as_str())
3023 .unwrap_or("")
3024 .to_string();
3025 let created = grain
3026 .fields
3027 .get("created_at")
3028 .and_then(|v| v.as_i64())
3029 .unwrap_or(0);
3030 groups
3031 .entry((subj, rel))
3032 .or_default()
3033 .push((created, idx, obj));
3034 }
3035 let mut remove_indices: std::collections::HashSet<usize> =
3036 std::collections::HashSet::new();
3037 for members in groups.values() {
3038 if members.len() < 2 {
3039 continue;
3040 }
3041 let has_diff = members.windows(2).any(|w| w[0].2 != w[1].2);
3043 if !has_diff {
3044 continue;
3045 }
3046 let best_idx = members
3048 .iter()
3049 .max_by_key(|(ts, _, _)| *ts)
3050 .map(|(_, idx, _)| *idx)
3051 .unwrap();
3052 for (_, idx, _) in members {
3053 if *idx != best_idx {
3054 remove_indices.insert(*idx);
3055 }
3056 }
3057 }
3058 if !remove_indices.is_empty() {
3059 let mut idx = 0;
3060 grains.retain(|_| {
3061 let keep = !remove_indices.contains(&idx);
3062 idx += 1;
3063 keep
3064 });
3065 }
3066 }
3067
3068 WithOption::Dedup { field: Some(field) } => {
3077 let mut seen: std::collections::HashSet<String> =
3078 std::collections::HashSet::new();
3079 grains.retain(|grain| match json_field(&grain.fields, field) {
3080 Some(v) => seen.insert(value_dedup_key(v)),
3081 None => true,
3082 });
3083 }
3084 WithOption::Dedup { field: None } => {
3085 let threshold = 0.85_f64;
3086 let mut seen_texts: Vec<String> = Vec::new();
3087 grains.retain(|grain| {
3088 let text = grain_result_text(grain);
3089 for prev in &seen_texts {
3090 if text_similarity(&text, prev) >= threshold {
3091 return false;
3092 }
3093 }
3094 seen_texts.push(text);
3095 true
3096 });
3097 }
3098
3099 WithOption::MinScore { score } => {
3109 grains.retain(|g| g.is_deterministic || g.score >= *score);
3110 }
3111
3112 WithOption::Diversity { .. } => {
3117 grains.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal));
3118 }
3119
3120 #[cfg(feature = "rerank")]
3124 WithOption::Rerank { ref model } => {
3125 if about_text.is_empty() {
3126 warnings.push(
3127 "WITH rerank on ASSEMBLE requires a topic — skipping \
3128 post-merge reranking"
3129 .into(),
3130 );
3131 } else if !grains.is_empty() {
3132 let texts: Vec<String> = grains.iter().map(grain_result_text).collect();
3133 let refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();
3134 let user_id = self.config.user_id_override.as_deref();
3135 let perm = store
3136 .rerank_passages(
3137 about_text,
3138 &refs,
3139 super::facade::RerankType::CrossEncoder,
3140 model.as_deref(),
3141 user_id,
3142 )
3143 .map_err(|e| CalError::BudgetExceeded {
3144 detail: format!("post-merge reranking failed: {}", e),
3145 span: None,
3146 })?;
3147 let mut reranked = Vec::with_capacity(grains.len());
3148 for &i in &perm {
3149 if let Some(g) = grains.get(i) {
3150 reranked.push(g.clone());
3151 }
3152 }
3153 grains = reranked;
3154 }
3155 }
3156
3157 #[cfg(feature = "llm-rerank")]
3160 WithOption::LlmRerank { ref model } => {
3161 if about_text.is_empty() {
3162 warnings.push(
3163 "WITH llm_rerank on ASSEMBLE requires a topic — skipping \
3164 post-merge reranking"
3165 .into(),
3166 );
3167 } else if !grains.is_empty() {
3168 let texts: Vec<String> = grains.iter().map(grain_result_text).collect();
3169 let refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();
3170 let user_id = self.config.user_id_override.as_deref();
3171 let perm = store
3172 .rerank_passages(
3173 about_text,
3174 &refs,
3175 super::facade::RerankType::Llm,
3176 model.as_deref(),
3177 user_id,
3178 )
3179 .map_err(|e| CalError::BudgetExceeded {
3180 detail: format!("post-merge LLM reranking failed: {}", e),
3181 span: None,
3182 })?;
3183 let mut reranked = Vec::with_capacity(grains.len());
3184 for &i in &perm {
3185 if let Some(g) = grains.get(i) {
3186 reranked.push(g.clone());
3187 }
3188 }
3189 grains = reranked;
3190 }
3191 }
3192
3193 _ => {}
3200 }
3201 }
3202
3203 let count = grains.len();
3204 Ok(CalResultPayload::Assembled {
3205 grains,
3206 sources,
3207 total_tokens,
3208 budget_limit,
3209 progressive: false,
3210 total_available: Some(count),
3211 })
3212 }
3213
3214 fn execute_exists(
3219 &self,
3220 exists: &ExistsStmt,
3221 store: &dyn CalStoreFacade,
3222 exec_warnings: &mut Vec<String>,
3223 let_values: &HashMap<String, Vec<String>>,
3224 ) -> std::result::Result<CalResultPayload, CalError> {
3225 let mut params = RecallParams::default();
3227
3228 if let Some(gt) = exists.grain_type.to_grain_type() {
3229 params.grain_type = Some(gt);
3230 }
3231
3232 if let Some(ref about) = exists.about {
3233 params.query = Some(about.text.clone());
3234 }
3235
3236 if let Some(ref where_clause) = exists.where_clause {
3237 self.apply_where_clause(&where_clause.condition, &mut params, exec_warnings, let_values)?;
3238 }
3239
3240 let hash_str =
3242 extract_hash_from_condition(exists.where_clause.as_ref().map(|w| &w.condition));
3243 if let Some(ref hs) = hash_str {
3244 let hash = Hash::from_hex(hs).map_err(|_| CalError::InvalidHash {
3245 found: hs.clone(),
3246 span: exists.span,
3247 })?;
3248 let found = store
3249 .exists(&hash)
3250 .map_err(|e| map_store_err(e, exists.span))?;
3251 return Ok(CalResultPayload::Exists {
3252 exists: found,
3253 hash: hs.clone(),
3254 });
3255 }
3256
3257 let residual_where = match exists.where_clause.as_ref() {
3264 Some(w) => plan_residual_where(&w.condition, &exists.grain_type, exec_warnings)?,
3265 None => None,
3266 };
3267
3268 params.limit = if residual_where.is_some() {
3271 Some(self.config.max_limit as usize)
3272 } else {
3273 Some(1)
3274 };
3275
3276 if let Some(ref ns) = self.config.namespace_override {
3278 params.namespace = Some(ns.clone());
3279 params.namespaces = None;
3280 }
3281 if let Some(ref uid) = self.config.user_id_override {
3282 params.user_id = Some(uid.clone());
3283 }
3284
3285 let hits = store
3286 .recall(¶ms)
3287 .map_err(|e| map_store_err(e, exists.span))?;
3288
3289 let (found, hash_out) = if let Some(ref residual) = residual_where {
3290 let grains = hits_to_grain_results(&hits);
3291 let first = grains
3292 .iter()
3293 .find(|g| grain_matches_condition_tree(g, residual));
3294 (first.is_some(), first.map(|g| g.hash.clone()).unwrap_or_default())
3295 } else {
3296 (
3297 !hits.is_empty(),
3298 hits.first().map(|h| h.hash.to_hex()).unwrap_or_default(),
3299 )
3300 };
3301
3302 Ok(CalResultPayload::Exists {
3303 exists: found,
3304 hash: hash_out,
3305 })
3306 }
3307
3308 fn execute_history(
3313 &self,
3314 history: &HistoryStmt,
3315 store: &dyn CalStoreFacade,
3316 exec_warnings: &mut Vec<String>,
3317 let_values: &HashMap<String, Vec<String>>,
3318 ) -> std::result::Result<CalResultPayload, CalError> {
3319 if let Some(ref diff_hash_str) = history.diff_target {
3324 let source_hash = Hash::from_hex(&history.hash).map_err(|_| CalError::InvalidHash {
3325 found: history.hash.clone(),
3326 span: history.span,
3327 })?;
3328 let target_hash = Hash::from_hex(diff_hash_str).map_err(|_| CalError::InvalidHash {
3329 found: diff_hash_str.clone(),
3330 span: history.span,
3331 })?;
3332
3333 let grain_a = store.get(&source_hash).map_err(|e| match e {
3336 AreevError::NotFound(_) => CalError::HashNotFound {
3339 hash: history.hash.clone(),
3340 span: history.span,
3341 },
3342 AreevError::CryptoError(_) => CalError::CryptoError {
3343 detail: format!("DIFF source grain decrypt failed: {}", e),
3344 span: history.span,
3345 },
3346 other => CalError::BudgetExceeded {
3347 detail: format!("DIFF source grain error: {}", other),
3348 span: history.span,
3349 },
3350 })?;
3351 let grain_b = store.get(&target_hash).map_err(|e| match e {
3352 AreevError::NotFound(_) => CalError::HashNotFound {
3354 hash: diff_hash_str.clone(),
3355 span: history.span,
3356 },
3357 AreevError::CryptoError(_) => CalError::CryptoError {
3358 detail: format!("DIFF target grain decrypt failed: {}", e),
3359 span: history.span,
3360 },
3361 other => CalError::BudgetExceeded {
3362 detail: format!("DIFF target grain error: {}", other),
3363 span: history.span,
3364 },
3365 })?;
3366
3367 let sub_a = grain_a.get_str("subject").unwrap_or("");
3369 let sub_b = grain_b.get_str("subject").unwrap_or("");
3370 let rel_a = grain_a.get_str("relation").unwrap_or("");
3371 let rel_b = grain_b.get_str("relation").unwrap_or("");
3372 if sub_a != sub_b || rel_a != rel_b {
3373 exec_warnings.push(
3374 "CAL-W005: DIFF targets have different subject/relation — diff may not be meaningful".to_string()
3375 );
3376 }
3377
3378 let changes = diff_grains(&grain_a, &grain_b);
3379 return Ok(CalResultPayload::Diff {
3380 source_hash: history.hash.clone(),
3381 target_hash: diff_hash_str.clone(),
3382 changes,
3383 });
3384 }
3385
3386 if history.hash.is_empty() {
3392 if let Some(ref wc) = history.where_clause {
3393 let mut params = RecallParams::default();
3394 self.apply_where_clause(&wc.condition, &mut params, exec_warnings, let_values)?;
3395
3396 let residual_where =
3400 plan_residual_where(&wc.condition, &GrainTypePlural::All, exec_warnings)?;
3401
3402 if let Some(ref ns) = self.config.namespace_override {
3404 params.namespace = Some(ns.clone());
3405 params.namespaces = None;
3406 }
3407 if let Some(ref uid) = self.config.user_id_override {
3408 params.user_id = Some(uid.clone());
3409 }
3410
3411 if params.limit.is_none() {
3415 params.limit = Some(if residual_where.is_some() {
3416 self.config.max_limit as usize
3417 } else {
3418 10
3419 });
3420 }
3421
3422 let hits = store
3423 .recall(¶ms)
3424 .map_err(|e| map_store_err(e, history.span))?;
3425
3426 let hits: Vec<crate::store_types::SearchHit> =
3429 if let Some(ref residual) = residual_where {
3430 let grains = hits_to_grain_results(&hits);
3431 hits.into_iter()
3432 .zip(grains)
3433 .filter(|(_, g)| grain_matches_condition_tree(g, residual))
3434 .map(|(h, _)| h)
3435 .collect()
3436 } else {
3437 hits
3438 };
3439
3440 let mut all_versions: Vec<CalVersionResult> = Vec::new();
3442 for hit in &hits {
3443 let ns = hit.grain.get_str("namespace").unwrap_or("");
3444 let subj = hit.grain.get_str("subject").unwrap_or("");
3445 let rel = hit.grain.get_str("relation").unwrap_or("");
3446
3447 if let Ok(entries) = store.get_history(ns, subj, rel) {
3448 for v in entries {
3449 if !all_versions
3452 .iter()
3453 .any(|existing| existing.hash == v.hash.to_hex())
3454 {
3455 all_versions.push(CalVersionResult {
3456 hash: v.hash.to_hex(),
3457 object: v.object,
3458 created_at: v.created_at,
3459 confidence: v.confidence,
3460 superseded_by: v
3461 .superseded_by
3462 .map(|h: areev_core::error::Hash| h.to_hex()),
3463 });
3464 }
3465 }
3466 }
3467 }
3468
3469 return Ok(CalResultPayload::History {
3470 versions: all_versions,
3471 });
3472 }
3473
3474 return Err(CalError::InvalidHash {
3476 found: String::new(),
3477 span: history.span,
3478 });
3479 }
3480
3481 let hash = Hash::from_hex(&history.hash).map_err(|_| CalError::InvalidHash {
3485 found: history.hash.clone(),
3486 span: history.span,
3487 })?;
3488
3489 let grain = match store.get(&hash) {
3491 Ok(g) => g,
3492 Err(AreevError::NotFound(_)) => {
3493 return Ok(CalResultPayload::Unsupported {
3494 statement: "history".into(),
3495 message: format!("grain not found: {}", history.hash),
3496 });
3497 }
3498 Err(other) => {
3499 return Err(map_store_err(other, history.span));
3500 }
3501 };
3502
3503 let namespace = grain.get_str("namespace").unwrap_or("");
3504 let subject = grain.get_str("subject").unwrap_or("");
3505 let relation = grain.get_str("relation").unwrap_or("");
3506
3507 let entries = store
3508 .get_history(namespace, subject, relation)
3509 .map_err(|e: areev_core::error::AreevError| map_store_err(e, history.span))?;
3510
3511 let versions: Vec<CalVersionResult> = entries
3512 .into_iter()
3513 .map(|v| CalVersionResult {
3514 hash: v.hash.to_hex(),
3515 object: v.object,
3516 created_at: v.created_at,
3517 confidence: v.confidence,
3518 superseded_by: v.superseded_by.map(|h: areev_core::error::Hash| h.to_hex()),
3519 })
3520 .collect();
3521
3522 Ok(CalResultPayload::History { versions })
3523 }
3524
3525 fn execute_describe(
3530 &self,
3531 describe: &DescribeStmt,
3532 store: &dyn CalStoreFacade,
3533 ) -> std::result::Result<CalResultPayload, CalError> {
3534 let info = match &describe.target {
3535 DescribeTarget::Schema => serde_json::json!({
3536 "grain_types": [
3537 "fact", "event", "state", "workflow", "tool", "recommendation",
3538 "observation", "goal", "reasoning", "consensus", "consent", "skill"
3539 ],
3540 "common_fields": [
3541 "subject", "relation", "object", "namespace", "user_id",
3542 "created_at", "confidence", "importance", "tags",
3543 "session_id", "content", "summary"
3544 ],
3545 "cal_version": 1,
3546 "tier1_enabled": self.config.tier1_enabled,
3547 "max_limit": self.config.max_limit,
3548 "default_limit": self.config.default_limit,
3549 "oms_version": "1.2",
3550 "pipeline_stages": [
3551 "SELECT", "ORDER BY", "LIMIT", "OFFSET", "COUNT",
3552 "FIRST", "SUBJECTS", "OBJECTS", "HASHES", "GROUP BY", "PROJECT"
3553 ],
3554 "with_options": [
3560 "superseded", "explanation", "annotate_relative_time",
3561 "conflict_resolution", "dedup", "contradiction_detection",
3562 "diversity", "multi_hop", "provenance", "query_expansion",
3563 "rerank"
3564 ]
3565 }),
3566 DescribeTarget::GrainType(gt) => {
3567 let type_name = gt.as_str();
3568 let specific_fields: &[&str] = match gt {
3569 GrainTypePlural::Triggers => &[
3573 "kind",
3574 "workflow",
3575 "connector",
3576 "scope",
3577 "enabled",
3578 "cron",
3579 ],
3580 GrainTypePlural::Facts => {
3581 &["subject", "relation", "object", "confidence", "session_id"]
3582 }
3583 GrainTypePlural::Events => &[
3584 "session_id",
3585 "run_id",
3586 "content",
3587 "created_at",
3588 "role",
3589 "parent_message_id",
3590 "model_id",
3591 "stop_reason",
3592 ],
3593 GrainTypePlural::Recommendations => &[
3598 "target_ref",
3599 "analyzer",
3600 "severity",
3601 "dedup_key",
3602 "rec_status",
3603 ],
3604 GrainTypePlural::States => &["context", "plan", "history"],
3605 GrainTypePlural::Workflows => &[
3606 "name",
3607 "nodes",
3608 "edges",
3609 "bindings",
3610 "retries",
3611 "trigger",
3612 "status",
3613 "session_id",
3614 ],
3615 GrainTypePlural::Tools => &[
3616 "tool",
3617 "input",
3618 "content",
3619 "is_error",
3620 "duration_ms",
3621 "session_id",
3622 ],
3623 GrainTypePlural::Observations => &["sensor", "value", "unit", "session_id"],
3624 GrainTypePlural::Goals => &[
3625 "title",
3626 "description",
3627 "priority",
3628 "status",
3629 "parent_hash",
3630 "session_id",
3631 ],
3632 GrainTypePlural::Reasonings => {
3633 &["premises", "conclusion", "confidence", "session_id"]
3634 }
3635 GrainTypePlural::Consensuses => {
3636 &["participants", "agreement", "confidence", "session_id"]
3637 }
3638 GrainTypePlural::Consents => &[
3639 "user_id",
3640 "scope",
3641 "granted",
3642 "expires_at",
3643 "subject_did",
3644 "grantee_did",
3645 "session_id",
3646 ],
3647 GrainTypePlural::Skills => &[
3648 "name",
3649 "description",
3650 "version",
3651 "domain",
3652 "holder_did",
3653 "proficiency",
3654 "transferable",
3655 "practice_count",
3656 "last_practiced_at",
3657 "session_id",
3658 ],
3659 GrainTypePlural::All => &[],
3660 };
3661 serde_json::json!({
3662 "grain_type": type_name,
3663 "specific_fields": specific_fields,
3664 "required_fields": crate::json_build::required_fields(
3669 gt.to_grain_type().map(|t| t.as_str()).unwrap_or("")
3670 ),
3671 "common_fields": [
3672 "namespace", "user_id", "created_at", "tags", "importance"
3673 ]
3674 })
3675 }
3676
3677 DescribeTarget::Capabilities => {
3679 let caps = store.describe_capabilities();
3680 serde_json::json!({
3681 "cal_version": caps.cal_version,
3682 "conformance_level": caps.conformance_level,
3683 "supported_statements": caps.supported_statements,
3684 "max_sources": caps.max_sources,
3685 "max_let_bindings": caps.max_let_bindings,
3686 "max_budget_tokens": caps.max_budget_tokens,
3687 "tier1_enabled": self.config.tier1_enabled,
3688 "oms_version": "1.2"
3689 })
3690 }
3691 DescribeTarget::Server => {
3692 let caps = store.describe_capabilities();
3693 serde_json::json!({
3694 "name": "areev",
3695 "version": env!("CARGO_PKG_VERSION"),
3696 "cal_version": caps.cal_version,
3697 "conformance_level": caps.conformance_level,
3698 "oms_version": "1.2",
3699 "build_features": build_features_list()
3700 })
3701 }
3702 DescribeTarget::Fields(opt_gt) => {
3703 let gt_engine = opt_gt.as_ref().and_then(|g| g.to_grain_type());
3704 let field_infos = store.describe_fields(gt_engine);
3705
3706 if !field_infos.is_empty() {
3709 let fields_json: Vec<serde_json::Value> = field_infos
3710 .iter()
3711 .map(|fi| {
3712 serde_json::json!({
3713 "name": fi.name,
3714 "type": fi.field_type,
3715 "filterable": fi.filterable,
3716 "sortable": fi.sortable,
3717 })
3718 })
3719 .collect();
3720 if let Some(gt) = opt_gt {
3721 serde_json::json!({
3722 "grain_type": gt.as_str(),
3723 "fields": fields_json
3724 })
3725 } else {
3726 serde_json::json!({
3727 "fields": fields_json
3728 })
3729 }
3730 } else {
3731 let mut fields: Vec<serde_json::Value> = vec![
3733 serde_json::json!({"name": "subject", "type": "string", "filterable": true, "sortable": true}),
3734 serde_json::json!({"name": "relation", "type": "string", "filterable": true, "sortable": true}),
3735 serde_json::json!({"name": "object", "type": "string", "filterable": true, "sortable": false}),
3736 serde_json::json!({"name": "namespace", "type": "string", "filterable": true, "sortable": true}),
3737 serde_json::json!({"name": "user_id", "type": "string", "filterable": true, "sortable": true}),
3738 serde_json::json!({"name": "created_at", "type": "timestamp", "filterable": true, "sortable": true}),
3739 serde_json::json!({"name": "confidence", "type": "number", "filterable": true, "sortable": true}),
3740 serde_json::json!({"name": "importance", "type": "number", "filterable": true, "sortable": true}),
3741 serde_json::json!({"name": "tags", "type": "array", "filterable": true, "sortable": false}),
3742 ];
3743 if let Some(gt) = gt_engine {
3750 for name in areev_core::types::registry::meta(gt).queryable_fields {
3751 if fields.iter().any(|f| f["name"] == *name) {
3752 continue;
3753 }
3754 fields.push(serde_json::json!({
3755 "name": name,
3756 "type": "field",
3757 "filterable": true,
3758 "sortable": false,
3759 }));
3760 }
3761 }
3762 if let Some(gt) = opt_gt {
3763 serde_json::json!({
3764 "grain_type": gt.as_str(),
3765 "fields": fields
3766 })
3767 } else {
3768 serde_json::json!({
3769 "fields": fields
3770 })
3771 }
3772 }
3773 }
3774 DescribeTarget::Templates => {
3775 let templates = store.list_templates();
3776 let template_list: Vec<serde_json::Value> = templates
3777 .iter()
3778 .map(|t| {
3779 serde_json::json!({
3780 "name": t.name,
3781 "description": t.description,
3782 "builtin": t.builtin,
3783 "parent": t.parent,
3784 })
3785 })
3786 .collect();
3787 serde_json::json!({ "templates": template_list })
3788 }
3789 DescribeTarget::Queries => {
3790 let queries = store.list_queries();
3791 serde_json::json!({
3792 "queries": queries,
3793 })
3794 }
3795 DescribeTarget::Query(name) => {
3796 let entry = store
3797 .get_query(name)
3798 .ok_or_else(|| CalError::QueryNotFound {
3799 name: name.clone(),
3800 span: None,
3801 })?;
3802 serde_json::json!({
3803 "name": name,
3804 "description": entry.description,
3805 "builtin": entry.builtin,
3806 "params": entry.params.iter().map(|p| {
3807 serde_json::json!({
3808 "name": p.name,
3809 "required": p.default.is_none(),
3810 "default": p.default.as_ref().map(|v| match v {
3811 super::ast::Value::String { value } => value.clone(),
3812 super::ast::Value::Number { value } => value.to_string(),
3813 super::ast::Value::Boolean { value } => value.to_string(),
3814 other => format!("{}", other),
3815 }),
3816 })
3817 }).collect::<Vec<_>>(),
3818 "body": entry.body,
3819 "body_size": entry.body.len(),
3820 })
3821 }
3822 DescribeTarget::Principal(name) => {
3826 let rows = store.cal_show_grants(Some(name)).map_err(|e| {
3827 let detail = e.to_string();
3828 if detail.contains("AUT-E") {
3829 CalError::NotAuthorized { detail, span: None }
3830 } else {
3831 CalError::InvalidQuery { detail, span: None }
3832 }
3833 })?;
3834 let grants: Vec<serde_json::Value> = rows
3835 .iter()
3836 .filter_map(|row| {
3837 let g =
3838 areev_core::authz::Grant::from_object_string(&row.object).ok()?;
3839 Some(serde_json::json!({
3840 "verbs": g.verbs.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
3841 "namespaces": g.namespaces,
3842 "hash": row.hash,
3843 }))
3844 })
3845 .collect();
3846 serde_json::json!({
3847 "principal": name,
3848 "grants": grants,
3849 "note": if grants.is_empty() {
3850 "no live grants — this principal can do nothing in a bound session"
3851 } else {
3852 "rights shown are the live grant heads; owner sessions bypass grants"
3853 },
3854 })
3855 }
3856 DescribeTarget::Loop
3859 | DescribeTarget::Analyzers
3860 | DescribeTarget::Outcomes
3861 | DescribeTarget::LoopPolicy => {
3862 let what = match &describe.target {
3863 DescribeTarget::Loop => crate::governance::LoopInfo::Loop,
3864 DescribeTarget::Analyzers => crate::governance::LoopInfo::Analyzers,
3865 DescribeTarget::Outcomes => crate::governance::LoopInfo::Outcomes,
3866 _ => crate::governance::LoopInfo::Policy,
3867 };
3868 match &self.governance {
3869 None => serde_json::json!({
3870 "error": "governance is not wired on this surface — no \
3871 GovernanceHost attached",
3872 }),
3873 Some(host) => host.describe(store, what).map_err(|e| {
3874 let detail = e.to_string();
3875 if detail.contains("AUT-E") {
3876 CalError::NotAuthorized { detail, span: None }
3877 } else {
3878 CalError::InvalidQuery { detail, span: None }
3879 }
3880 })?,
3881 }
3882 }
3883 DescribeTarget::Stats | DescribeTarget::Integrity => {
3884 let res = if matches!(&describe.target, DescribeTarget::Stats) {
3885 store.cal_stats()
3886 } else {
3887 store.cal_verify()
3888 };
3889 res.map_err(|e| {
3890 let detail = e.to_string();
3891 if detail.contains("AUT-E") {
3892 CalError::NotAuthorized { detail, span: None }
3893 } else {
3894 CalError::InvalidQuery { detail, span: None }
3895 }
3896 })?
3897 }
3898 DescribeTarget::Grammar => serde_json::json!({
3899 "grammar": "CAL/1 grammar (simplified BNF)",
3900 "version": 1,
3901 "conformance_level": 2,
3902 "features": [
3903 "RECALL", "EXISTS", "ASSEMBLE", "HISTORY", "HISTORY DIFF",
3904 "EXPLAIN", "DESCRIBE", "BATCH", "COALESCE",
3905 "SET operations (UNION, INTERSECT, EXCEPT)",
3906 "Pipeline stages (SELECT, ORDER BY, LIMIT, OFFSET, COUNT, FIRST, SUBJECTS, OBJECTS, HASHES, GROUP BY, PROJECT)",
3907 "WITH options (superseded, score_breakdown, explanation, contradiction_detection, diversity)",
3908 "LET bindings", "WHERE clause", "ABOUT semantic search",
3909 "SINCE / BETWEEN temporal filters", "RECENT shorthand"
3910 ],
3911 "url": "https://github.com/AreevAI/areev"
3912 }),
3913 };
3914
3915 Ok(CalResultPayload::Describe { info })
3916 }
3917
3918 fn execute_explain(
3923 &self,
3924 explain: &ExplainStmt,
3925 store: &dyn CalStoreFacade,
3926 query: &CalQuery,
3927 ) -> std::result::Result<CalResultPayload, CalError> {
3928 let inner = explain.inner.as_ref();
3929 let stmt_type = statement_type_name(inner);
3930
3931 let (grain_type, query_routing, index_usage, mut filters) = match inner {
3932 CalStatement::Recall(recall) => {
3933 let gt = recall
3934 .grain_type
3935 .to_grain_type()
3936 .map(|g| g.as_str().to_string());
3937
3938 let mut index_usage = Vec::new();
3939 let mut filters = Vec::new();
3940
3941 let free_text = recall.about.is_some() || recall.like.is_some();
3948
3949 if free_text {
3950 index_usage.push("bm25_fts".to_string());
3951 }
3952
3953 if let Some(ref wc) = recall.where_clause {
3954 collect_filter_names(&wc.condition, &mut filters);
3955 if filters
3957 .iter()
3958 .any(|f| f == "subject" || f == "relation" || f == "object")
3959 {
3960 index_usage.push("hexastore".to_string());
3961 }
3962 }
3963
3964 if recall.since.is_some() || recall.until.is_some() || recall.between.is_some() {
3965 filters.push("temporal".to_string());
3966 }
3967
3968 let routing = if free_text && !index_usage.contains(&"hexastore".to_string()) {
3970 "bm25".to_string()
3971 } else if free_text {
3972 "hybrid_rrf".to_string()
3973 } else {
3974 "structural".to_string()
3975 };
3976
3977 (gt, routing, index_usage, filters)
3978 }
3979 CalStatement::Exists(exists) => {
3980 let gt = exists
3981 .grain_type
3982 .to_grain_type()
3983 .map(|g| g.as_str().to_string());
3984 let routing = "structural".to_string();
3985 let index_usage = vec!["hexastore".to_string()];
3986 let mut filters = Vec::new();
3987 if let Some(ref wc) = exists.where_clause {
3988 collect_filter_names(&wc.condition, &mut filters);
3989 }
3990 (gt, routing, index_usage, filters)
3991 }
3992 CalStatement::History(h) => {
3993 let mut filters = vec!["hash".to_string()];
3994 if h.diff_target.is_some() {
3995 filters.push("diff_target".to_string());
3996 }
3997 if h.where_clause.is_some() {
3998 filters.push("where_clause".to_string());
3999 }
4000 (
4001 None,
4002 if h.diff_target.is_some() {
4003 "diff_comparison".to_string()
4004 } else {
4005 "entity_latest".to_string()
4006 },
4007 vec![
4008 "entity_latest".to_string(),
4009 "supersession_chain".to_string(),
4010 ],
4011 filters,
4012 )
4013 }
4014 CalStatement::Assemble(assemble) => {
4015 let mut index_usage = Vec::new();
4016 let mut filters = Vec::new();
4017
4018 let source_count = assemble.sources.as_ref().map_or(1, |s| s.len());
4020 filters.push(format!("sources: {}", source_count));
4021
4022 if assemble.budget.is_some() {
4023 filters.push("budget_allocation".to_string());
4024 }
4025 for opt in &assemble.assemble_with {
4026 let super::ast::AssembleWithOption::Dedup { .. } = opt;
4027 filters.push("dedup".to_string());
4028 }
4029 index_usage.push("bm25_fts".to_string());
4030 index_usage.push("hexastore".to_string());
4031
4032 (
4033 None,
4034 format!("multi_source_assemble({}_sources)", source_count),
4035 index_usage,
4036 filters,
4037 )
4038 }
4039 CalStatement::Coalesce(coalesce) => {
4040 let gt = coalesce
4041 .grain_type
4042 .to_grain_type()
4043 .map(|g| g.as_str().to_string());
4044 let branch_count = if coalesce.branches.is_empty() {
4045 1
4046 } else {
4047 coalesce.branches.len()
4048 };
4049 let has_else = coalesce.else_branch.is_some();
4050 let mut filters = vec![format!("fallback_chain: {} branches", branch_count)];
4051 if has_else {
4052 filters.push("else_fallback".to_string());
4053 }
4054 (
4055 gt,
4056 "coalesce_fallback".to_string(),
4057 vec!["bm25_fts".to_string(), "hexastore".to_string()],
4058 filters,
4059 )
4060 }
4061 CalStatement::Batch(batch) => {
4062 let stmt_count = batch.statements.len();
4063 let has_labels = batch.labeled.is_some();
4064 let has_formats = batch.statements.iter().any(|e| e.format.is_some());
4065 let has_pipelines = batch.statements.iter().any(|e| !e.pipeline.is_empty());
4066 let mut filters = vec![format!("parallel_execution: {} statements", stmt_count)];
4067 if has_labels {
4068 filters.push("labeled_results".to_string());
4069 }
4070 if has_formats {
4071 filters.push("per_entry_format".to_string());
4072 }
4073 if has_pipelines {
4074 filters.push("per_entry_pipeline".to_string());
4075 }
4076 (None, "parallel_batch".to_string(), vec![], filters)
4077 }
4078 CalStatement::Describe(_) => (None, "introspection".to_string(), vec![], vec![]),
4079 CalStatement::ReportSubject(rs) => {
4080 let mut filters = vec![format!("subject: {}", rs.subject_id)];
4081 if rs.text_mentions {
4082 filters.push("text_mentions".to_string());
4083 }
4084 (
4085 None,
4086 "subject_report".to_string(),
4087 vec!["terms_dictionary".to_string(), "fts_postings".to_string()],
4088 filters,
4089 )
4090 }
4091 CalStatement::Purge(purge) => {
4092 let mut filters = vec![];
4093 if let Some(age) = purge.min_age_days {
4094 filters.push(format!("min_age_days: {age}"));
4095 }
4096 if let Some(ref ns) = purge.namespace {
4097 filters.push(format!("namespace: {ns}"));
4098 }
4099 if let Some(lim) = purge.limit {
4100 filters.push(format!("batch_limit: {lim}"));
4101 }
4102 (
4103 None,
4104 "destructive_purge_stale".to_string(),
4105 vec!["blobs_partition".to_string(), "decay_engine".to_string()],
4106 filters,
4107 )
4108 }
4109 CalStatement::Forget(forget) => {
4110 let target_desc = match &forget.target {
4111 super::ast::ForgetTarget::Hash { hash } => format!("hash: {hash}"),
4112 super::ast::ForgetTarget::User { user_id } => format!("user: {user_id}"),
4113 super::ast::ForgetTarget::Scope { scope } => format!("scope: {scope}"),
4114 };
4115 let indexes = match &forget.target {
4116 super::ast::ForgetTarget::Hash { .. } => vec!["blobs_partition".to_string()],
4117 _ => vec!["blobs_partition".to_string(), "key_store".to_string()],
4118 };
4119 (
4120 None,
4121 "destructive_forget".to_string(),
4122 indexes,
4123 vec![target_desc],
4124 )
4125 }
4126 CalStatement::DefineTemplate(def) => (
4127 None,
4128 "template_registry_write".to_string(),
4129 vec!["meta_partition".to_string()],
4130 vec![format!("template_name: {}", def.name)],
4131 ),
4132 CalStatement::DropTemplate(drop) => (
4133 None,
4134 "template_registry_delete".to_string(),
4135 vec!["meta_partition".to_string()],
4136 vec![format!("template_name: {}", drop.name)],
4137 ),
4138 CalStatement::DefineQuery(def) => (
4139 None,
4140 "query_registry_write".to_string(),
4141 vec!["meta_partition".to_string()],
4142 vec![format!("query_name: {}", def.name)],
4143 ),
4144 CalStatement::DropQuery(drop) => (
4145 None,
4146 "query_registry_delete".to_string(),
4147 vec!["meta_partition".to_string()],
4148 vec![format!("query_name: {}", drop.name)],
4149 ),
4150 CalStatement::RunQuery(run) => (
4151 None,
4152 "saved_query_execute".to_string(),
4153 vec!["meta_partition".to_string()],
4154 vec![format!("query_name: {}", run.name)],
4155 ),
4156 _ => (None, "unknown".to_string(), vec![], vec![]),
4157 };
4158
4159 let mut pipeline_steps = vec![format!("execute_{}", stmt_type)];
4161 for stage in &query.pipeline {
4162 pipeline_steps.push(pipeline_stage_name(stage));
4163 }
4164
4165 let mut policy_filters = Vec::new();
4170
4171 if self.config.namespace_override.is_some() {
4172 policy_filters.push("namespace_override (capability token)".to_string());
4173 } else if store.default_namespace().is_some() {
4174 policy_filters.push("namespace_scope (session)".to_string());
4175 }
4176
4177 if self.config.user_id_override.is_some() {
4178 policy_filters.push("user_id_override (capability token)".to_string());
4179 } else if store.active_user().is_some() {
4180 policy_filters.push("user_id_scope (session)".to_string());
4181 }
4182
4183 if !self.config.tier1_enabled {
4184 policy_filters.push("tier1_disabled (read-only mode)".to_string());
4185 }
4186
4187 filters.extend(policy_filters);
4189
4190 let ns_context = if self.config.namespace_override.is_some() {
4192 "(namespace: from capability token)"
4193 } else if store.default_namespace().is_some() {
4194 "(namespace: from session)"
4195 } else {
4196 "(namespace: not set)"
4197 };
4198
4199 let cost = match index_usage.len() {
4200 0 => "O(n) full scan",
4201 1 => "O(log n) index lookup",
4202 _ => "O(log n) hybrid with RRF fusion",
4203 };
4204
4205 Ok(CalResultPayload::Explain {
4206 plan: CalQueryPlan {
4207 statement_type: stmt_type,
4208 grain_type,
4209 query_routing,
4210 index_usage,
4211 estimated_cost: format!("{} {}", cost, ns_context),
4212 filters,
4213 pipeline: pipeline_steps,
4214 },
4215 })
4216 }
4217
4218 fn execute_batch(
4223 &self,
4224 batch: &BatchStmt,
4225 store: &dyn CalStoreFacade,
4226 exec_warnings: &mut Vec<String>,
4227 ) -> std::result::Result<CalResultPayload, CalError> {
4228 if let Some(ref labeled) = batch.labeled {
4233 let mut results: HashMap<String, CalResultPayload> = HashMap::new();
4234 let mut seen_labels: std::collections::HashSet<String> =
4235 std::collections::HashSet::new();
4236
4237 for (label, entry) in labeled {
4238 if !seen_labels.insert(label.clone()) {
4240 return Err(CalError::AssembleDuplicateLabel {
4241 label: label.clone(),
4242 span: batch.span,
4243 });
4244 }
4245
4246 let payload = self.execute_batch_entry(entry, store, exec_warnings)?;
4247 results.insert(label.clone(), payload);
4248 }
4249
4250 return Ok(CalResultPayload::Batch { results });
4251 }
4252
4253 let mut results: HashMap<String, CalResultPayload> = HashMap::new();
4255
4256 for (idx, entry) in batch.statements.iter().enumerate() {
4257 let payload = self.execute_batch_entry(entry, store, exec_warnings)?;
4258 results.insert(idx.to_string(), payload);
4259 }
4260
4261 Ok(CalResultPayload::Batch { results })
4262 }
4263
4264 fn execute_batch_entry(
4266 &self,
4267 entry: &BatchEntry,
4268 store: &dyn CalStoreFacade,
4269 exec_warnings: &mut Vec<String>,
4270 ) -> std::result::Result<CalResultPayload, CalError> {
4271 let surrogate_query = crate::ast::CalQuery {
4272 version: crate::ast::CalVersion(1),
4273 statement: entry.statement.clone(),
4274 pipeline: entry.pipeline.clone(),
4275 with_options: entry.with_options.clone(),
4276 format: entry.format.clone(),
4277 let_bindings: Vec::new(),
4278 let_values: HashMap::new(),
4279 user_vars: entry.user_vars.clone(),
4280 warnings: Vec::new(),
4281 };
4282
4283 let payload = self
4284 .execute_statement(&entry.statement, store, &surrogate_query, exec_warnings)
4285 .unwrap_or_else(|e| CalResultPayload::Unsupported {
4286 statement: statement_type_name(&entry.statement),
4287 message: e.to_string(),
4288 });
4289
4290 let (payload, grouped_by) = if entry.pipeline.is_empty() {
4292 (payload, None)
4293 } else {
4294 self.apply_pipeline(payload, &entry.pipeline, exec_warnings)?
4295 };
4296
4297 let payload = apply_format_clause(
4299 payload,
4300 &entry.format,
4301 grouped_by.as_deref(),
4302 RenderInputs {
4303 user_vars: &entry.user_vars,
4304 store,
4305 disclosure: disclosure_of(&entry.with_options),
4306 },
4307 None,
4308 exec_warnings,
4309 )?;
4310
4311 Ok(payload)
4312 }
4313
4314 fn execute_coalesce(
4319 &self,
4320 coalesce: &CoalesceStmt,
4321 store: &dyn CalStoreFacade,
4322 query: &CalQuery,
4323 exec_warnings: &mut Vec<String>,
4324 ) -> std::result::Result<CalResultPayload, CalError> {
4325 if !coalesce.branches.is_empty() {
4331 for (i, branch) in coalesce.branches.iter().enumerate() {
4332 let surrogate = CalQuery {
4333 version: query.version,
4334 statement: branch.query.clone(),
4335 pipeline: Vec::new(),
4336 with_options: query.with_options.clone(),
4337 format: None,
4338 let_bindings: Vec::new(),
4339 let_values: query.let_values.clone(),
4340 user_vars: HashMap::new(),
4341 warnings: Vec::new(),
4342 };
4343
4344 let payload =
4345 self.execute_statement(&surrogate.statement, store, &surrogate, exec_warnings)?;
4346
4347 let is_non_empty = match &payload {
4349 CalResultPayload::Grains { grains, .. } => !grains.is_empty(),
4350 CalResultPayload::Exists { exists, .. } => *exists,
4351 CalResultPayload::History { versions } => !versions.is_empty(),
4352 CalResultPayload::Count { count } => *count > 0,
4353 _ => true,
4354 };
4355
4356 if is_non_empty {
4357 exec_warnings.push(format!(
4358 "COALESCE: branch {} returned results; short-circuited {} remaining branch(es)",
4359 i + 1,
4360 coalesce.branches.len() - i - 1
4361 + if coalesce.else_branch.is_some() { 1 } else { 0 }
4362 ));
4363 return Ok(payload);
4364 }
4365 }
4366
4367 if let Some(ref else_stmt) = coalesce.else_branch {
4369 let surrogate = CalQuery {
4370 version: query.version,
4371 statement: *else_stmt.clone(),
4372 pipeline: Vec::new(),
4373 with_options: query.with_options.clone(),
4374 format: None,
4375 let_bindings: Vec::new(),
4376 let_values: query.let_values.clone(),
4377 user_vars: HashMap::new(),
4378 warnings: Vec::new(),
4379 };
4380
4381 return self.execute_statement(
4382 &surrogate.statement,
4383 store,
4384 &surrogate,
4385 exec_warnings,
4386 );
4387 }
4388
4389 return Ok(CalResultPayload::Grains {
4391 grains: Vec::new(),
4392 total_available: Some(0),
4393 });
4394 }
4395
4396 let recall = RecallStmt {
4400 grain_type: coalesce.grain_type.clone(),
4401 about: None,
4402 where_clause: coalesce.where_clause.clone(),
4403 recent: None,
4404 since: None,
4405 until: None,
4406 like: None,
4407 between: None,
4408 contradictions: None,
4409 limit: None,
4410 as_format: None,
4411 span: coalesce.span,
4412 };
4413
4414 let payload = self.execute_recall(&recall, store, query, exec_warnings)?;
4415
4416 match payload {
4418 CalResultPayload::Grains { mut grains, .. } => {
4419 grains.truncate(1);
4420 let count = grains.len();
4421 Ok(CalResultPayload::Grains {
4422 grains,
4423 total_available: Some(count),
4424 })
4425 }
4426 other => Ok(other),
4427 }
4428 }
4429
4430 fn execute_set_op(
4435 &self,
4436 set_op: &SetOpStmt,
4437 store: &dyn CalStoreFacade,
4438 query: &CalQuery,
4439 exec_warnings: &mut Vec<String>,
4440 ) -> std::result::Result<CalResultPayload, CalError> {
4441 let mut operand_results: Vec<Vec<CalGrainResult>> = Vec::new();
4443
4444 for stmt in &set_op.operands {
4445 let surrogate = crate::ast::CalQuery {
4446 version: query.version,
4447 statement: stmt.clone(),
4448 pipeline: Vec::new(),
4449 with_options: query.with_options.clone(),
4450 format: None,
4451 let_bindings: Vec::new(),
4452 let_values: query.let_values.clone(),
4453 user_vars: HashMap::new(),
4454 warnings: Vec::new(),
4455 };
4456 let payload = self.execute_statement(stmt, store, &surrogate, exec_warnings)?;
4457 let grains = extract_grains(payload);
4458 operand_results.push(grains);
4459 }
4460
4461 if operand_results.is_empty() {
4462 return Ok(CalResultPayload::Grains {
4463 grains: Vec::new(),
4464 total_available: Some(0),
4465 });
4466 }
4467
4468 let mut result = operand_results.remove(0);
4469
4470 for next in operand_results {
4471 result = match set_op.op {
4472 SetOp::Union => union_grains(result, next),
4473 SetOp::Intersect => intersect_grains(result, &next),
4474 SetOp::Except => except_grains(result, &next),
4475 };
4476 }
4477
4478 let count = result.len();
4479 Ok(CalResultPayload::Grains {
4480 grains: result,
4481 total_available: Some(count),
4482 })
4483 }
4484
4485 fn sample_assembly_manifest(&self) -> bool {
4490 let rate = self.config.assembly_manifest_sample_rate.clamp(0.0, 1.0);
4491 if rate <= 0.0 {
4492 return false;
4493 }
4494 if rate >= 1.0 {
4495 return true;
4496 }
4497 use std::sync::atomic::{AtomicU64, Ordering};
4498 static SAMPLE_SEQ: AtomicU64 = AtomicU64::new(0);
4499 let n = SAMPLE_SEQ.fetch_add(1, Ordering::Relaxed);
4502 let bucket = n.wrapping_mul(0x9e3779b97f4a7c15) % 1_000_000;
4503 bucket < (rate * 1_000_000.0) as u64
4504 }
4505
4506 #[allow(clippy::too_many_arguments)]
4509 fn record_assembly_manifest(
4510 &self,
4511 assemble: &AssembleStmt,
4512 store: &dyn CalStoreFacade,
4513 candidates: Vec<String>,
4514 included: Vec<String>,
4515 payload: &CalResultPayload,
4516 budget: serde_json::Value,
4517 sources: serde_json::Value,
4518 ) {
4519 if !self.sample_assembly_manifest() {
4520 return;
4521 }
4522 let included_set: std::collections::HashSet<&str> =
4523 included.iter().map(String::as_str).collect();
4524 let dropped_hashes = candidates
4525 .into_iter()
4526 .filter(|h| !included_set.contains(h.as_str()))
4527 .collect();
4528 let rendered = match payload {
4529 CalResultPayload::Formatted { text, .. } => text.as_bytes().to_vec(),
4530 CalResultPayload::MultiFormatted { formats, .. } => {
4531 let sorted: std::collections::BTreeMap<&str, &str> = formats
4532 .iter()
4533 .map(|(k, v)| (k.as_str(), v.as_str()))
4534 .collect();
4535 serde_json::to_vec(&sorted).unwrap_or_default()
4536 }
4537 _ => serde_json::to_vec(payload).unwrap_or_default(),
4538 };
4539 let mut digest = Sha256::new();
4540 digest.update(&rendered);
4541 let mut query_digest = Sha256::new();
4545 query_digest.update(
4546 serde_json::to_vec(assemble)
4547 .unwrap_or_default()
4548 .as_slice(),
4549 );
4550 store.note_assembly_manifest(&AssemblyManifest {
4551 query_sha256: hex::encode(query_digest.finalize()),
4552 included_hashes: included,
4553 rendered_sha256: hex::encode(digest.finalize()),
4554 budget,
4555 dropped_hashes,
4556 sources,
4557 });
4558 }
4559
4560 fn execute_assemble(
4561 &self,
4562 assemble: &AssembleStmt,
4563 store: &dyn CalStoreFacade,
4564 query: &CalQuery,
4565 exec_warnings: &mut Vec<String>,
4566 ) -> std::result::Result<CalResultPayload, CalError> {
4567 let merged;
4574 let query = if assemble.with_options.is_empty() {
4575 query
4576 } else {
4577 merged = CalQuery {
4578 with_options: query
4579 .with_options
4580 .iter()
4581 .chain(&assemble.with_options)
4582 .cloned()
4583 .collect(),
4584 ..query.clone()
4585 };
4586 &merged
4587 };
4588 if assemble.streaming {
4592 return Ok(CalResultPayload::StreamAssemble {
4593 assemble: Box::new(assemble.clone()),
4594 with_options: query.with_options.clone(),
4595 });
4596 }
4597
4598 if assemble.sources.is_some() {
4604 let engine = super::assemble::AssembleEngine::new(self);
4605 let result = engine.execute(assemble, store, query, exec_warnings)?;
4606 let (candidates, source_snapshot, budget_snapshot) = match &result {
4607 CalResultPayload::Assembled {
4608 grains,
4609 sources,
4610 total_tokens,
4611 budget_limit,
4612 ..
4613 } => {
4614 let mut all = Vec::new();
4615 let mut offset = 0usize;
4616 for source in sources {
4617 let end = (offset + source.grain_count).min(grains.len());
4618 all.extend(grains[offset..end].iter().map(|g| g.hash.clone()));
4619 all.extend(source.omitted.iter().map(|g| g.hash.clone()));
4620 offset = end;
4621 }
4622 (
4623 all,
4624 serde_json::to_value(sources).unwrap_or(serde_json::Value::Null),
4625 serde_json::json!({
4626 "limit": budget_limit,
4627 "unit": "tokens",
4628 "used": total_tokens,
4629 }),
4630 )
4631 }
4632 _ => (Vec::new(), serde_json::Value::Null, serde_json::Value::Null),
4633 };
4634
4635 let result = if !query.with_options.is_empty() {
4644 self.apply_assemble_post_merge_options(
4645 result,
4646 &query.with_options,
4647 exec_warnings,
4648 store,
4649 &assemble.topic,
4650 )?
4651 } else {
4652 result
4653 };
4654
4655 let included = match &result {
4656 CalResultPayload::Assembled { grains, .. } => {
4657 grains.iter().map(|g| g.hash.clone()).collect()
4658 }
4659 _ => Vec::new(),
4660 };
4661
4662 let result = if assemble.format.is_some() {
4664 apply_format_clause(
4665 result,
4666 &assemble.format,
4667 None,
4668 RenderInputs {
4669 user_vars: &query.user_vars,
4670 store,
4671 disclosure: disclosure_of(&assemble.with_options)
4675 .or_else(|| disclosure_of(&query.with_options)),
4676 },
4677 Some((
4681 assemble.context_name.as_deref().unwrap_or(&assemble.topic),
4682 assemble.for_whom.as_deref().unwrap_or(""),
4683 )),
4684 exec_warnings,
4685 )?
4686 } else {
4687 result
4688 };
4689 self.record_assembly_manifest(
4690 assemble,
4691 store,
4692 candidates,
4693 included,
4694 &result,
4695 budget_snapshot,
4696 source_snapshot,
4697 );
4698 return Ok(result);
4699 }
4700
4701 let base_grains = match &assemble.from {
4706 Source::Query(recall_stmt) => {
4707 let surrogate = crate::ast::CalQuery {
4708 version: query.version,
4709 statement: CalStatement::Recall(*recall_stmt.clone()),
4710 pipeline: Vec::new(),
4711 with_options: query.with_options.clone(),
4712 format: None,
4713 let_bindings: Vec::new(),
4714 let_values: query.let_values.clone(),
4715 user_vars: HashMap::new(),
4716 warnings: Vec::new(),
4717 };
4718 let payload =
4719 self.execute_statement(&surrogate.statement, store, &surrogate, exec_warnings)?;
4720 extract_grains(payload)
4721 }
4722 Source::Hashes(hashes) => {
4723 let mut grains = Vec::new();
4724 for hs in hashes {
4725 match Hash::from_hex(hs) {
4726 Ok(hash) => {
4727 if let Ok(grain) = store.get(&hash) {
4728 grains.push(CalGrainResult {
4729 hash: hs.clone(),
4730 grain_type: grain.grain_type.as_str().to_string(),
4731 score: 1.0,
4732 fields: serde_json::Value::Object(
4733 grain
4734 .fields
4735 .iter()
4736 .map(|(k, v)| (k.clone(), v.clone()))
4737 .collect(),
4738 ),
4739 score_breakdown: None,
4740 explanation: None,
4741 relative_time: None,
4742 is_deterministic: true,
4743 contested_by: None,
4744 });
4745 }
4746 }
4747 Err(_) => {
4748 }
4750 }
4751 }
4752 grains
4753 }
4754 Source::Parameter { name, .. } => {
4755 return Ok(CalResultPayload::Unsupported {
4756 statement: "assemble".into(),
4757 message: format!(
4758 "Parameter source ${} is not yet supported in single-source ASSEMBLE. \
4759 Use a RECALL subquery instead.",
4760 name
4761 ),
4762 });
4763 }
4764 };
4765
4766 let grains = if let Some(ref wc) = assemble.where_clause {
4771 validate_residual_subtree(
4778 &wc.condition,
4779 &GrainTypePlural::All,
4780 "in ASSEMBLE WHERE (a post-composition filter)",
4781 exec_warnings,
4782 )?;
4783 base_grains
4784 .into_iter()
4785 .filter(|grain| grain_matches_condition_tree(grain, &wc.condition))
4786 .collect()
4787 } else {
4788 base_grains
4789 };
4790
4791 let candidates: Vec<String> = grains.iter().map(|g| g.hash.clone()).collect();
4800 let (grains, omitted) = if let Some(ref budget) = assemble.budget {
4801 let mut grains = grains;
4802 let limit = budget.tokens as usize;
4803 if grains.len() > limit {
4804 let dropped = grains.split_off(limit);
4805 (grains, dropped)
4806 } else {
4807 (grains, Vec::new())
4808 }
4809 } else {
4810 (grains, Vec::new())
4811 };
4812 store.note_assembly_budget(!omitted.is_empty());
4813 let included: Vec<String> = grains.iter().map(|g| g.hash.clone()).collect();
4814 let budget_snapshot = serde_json::json!({
4815 "limit": assemble.budget.as_ref().map(|b| b.tokens),
4816 "unit": "grains",
4817 "used": grains.len(),
4818 });
4819 let source_snapshot = serde_json::json!([{
4820 "label": "",
4821 "grain_count": grains.len(),
4822 "dropped_count": omitted.len(),
4823 }]);
4824
4825 let result = if let Some(ref clause) = assemble.format {
4830 let ctx = super::templates::AssemblyContext {
4835 name: assemble
4836 .context_name
4837 .clone()
4838 .unwrap_or_else(|| assemble.topic.clone()),
4839 intent: assemble.for_whom.clone().unwrap_or_default(),
4840 source_count: 1,
4841 grain_count: grains.len(),
4842 budget_total: assemble.budget.as_ref().map_or(0, |b| b.tokens as u64),
4843 budget_used: grains.len() as u64,
4844 budget_unit: "grains".to_string(),
4847 };
4848 let sources = [super::templates::RenderSource {
4849 label: "",
4850 grains: &grains,
4851 omitted: &omitted,
4852 priority: 1,
4853 tokens_used: grains.len(),
4854 truncated: !omitted.is_empty(),
4855 }];
4856 let plan = super::templates::RenderPlan {
4857 assembly: Some(&ctx),
4858 sources: Some(&sources),
4859 };
4860 apply_format_clause_to_grains(
4861 &grains,
4862 clause,
4863 None,
4864 RenderInputs {
4865 user_vars: &query.user_vars,
4866 store,
4867 disclosure: disclosure_of(&query.with_options),
4868 },
4869 &plan,
4870 exec_warnings,
4871 )?
4872 } else {
4873 let count = grains.len();
4874 CalResultPayload::Grains {
4875 grains,
4876 total_available: Some(count),
4877 }
4878 };
4879 self.record_assembly_manifest(
4880 assemble,
4881 store,
4882 candidates,
4883 included,
4884 &result,
4885 budget_snapshot,
4886 source_snapshot,
4887 );
4888 Ok(result)
4889 }
4890
4891 fn validate_pipeline_fields(
4899 stages: &[PipelineStage],
4900 grain_type: &GrainTypePlural,
4901 ) -> std::result::Result<(), CalError> {
4902 let check = |field: &str, span: Option<Span>| -> std::result::Result<(), CalError> {
4903 if COMMON_FIELDS.contains(&field) {
4905 return Ok(());
4906 }
4907 if field.contains(':') {
4910 return Ok(());
4911 }
4912 let allowed = type_specific_fields(grain_type);
4914 if allowed.contains(&field) {
4915 return Ok(());
4916 }
4917 let suggestion = suggest_field(field, grain_type);
4920 Err(CalError::FieldNotOnGrainType {
4921 field: field.to_string(),
4922 grain_type: grain_type.as_str().to_string(),
4923 span,
4924 suggestion,
4925 })
4926 };
4927
4928 for stage in stages {
4929 match stage {
4930 PipelineStage::Select { fields, span } => {
4931 for f in fields {
4932 check(f, *span)?;
4933 }
4934 }
4935 PipelineStage::OrderBy { field, span, .. } => check(field, *span)?,
4936 PipelineStage::GroupBy { field, span } => check(field, *span)?,
4937 PipelineStage::Project { fields, span } => {
4938 for pf in fields {
4939 check(&pf.field, *span)?;
4940 }
4941 }
4942 _ => {}
4943 }
4944 }
4945 Ok(())
4946 }
4947
4948 fn apply_pipeline(
4949 &self,
4950 payload: CalResultPayload,
4951 stages: &[PipelineStage],
4952 exec_warnings: &mut Vec<String>,
4953 ) -> std::result::Result<(CalResultPayload, Option<String>), CalError> {
4954 let mut current = payload;
4955 let mut grouped_by: Option<String> = None;
4956
4957 for stage in stages {
4958 current = match (current, stage) {
4959 (CalResultPayload::Grains { grains, .. }, PipelineStage::Limit { value, .. }) => {
4961 let capped = (*value).min(self.config.max_limit) as usize;
4962 let limited: Vec<_> = grains.into_iter().take(capped).collect();
4963 let count = limited.len();
4964 CalResultPayload::Grains {
4965 grains: limited,
4966 total_available: Some(count),
4967 }
4968 }
4969
4970 (CalResultPayload::Grains { grains, .. }, PipelineStage::Offset { value, .. }) => {
4972 let offset: Vec<_> = grains.into_iter().skip(*value as usize).collect();
4973 let count = offset.len();
4974 CalResultPayload::Grains {
4975 grains: offset,
4976 total_available: Some(count),
4977 }
4978 }
4979
4980 (CalResultPayload::Grains { grains, .. }, PipelineStage::Count { .. }) => {
4982 CalResultPayload::Count {
4983 count: grains.len(),
4984 }
4985 }
4986
4987 (CalResultPayload::Grains { grains, .. }, PipelineStage::First { .. }) => {
4989 let first: Vec<_> = grains.into_iter().take(1).collect();
4990 CalResultPayload::Grains {
4991 grains: first,
4992 total_available: Some(1),
4993 }
4994 }
4995
4996 (
4998 CalResultPayload::Grains { grains, .. },
4999 PipelineStage::OrderBy {
5000 field, descending, ..
5001 },
5002 ) => {
5003 let mut sorted = grains;
5004 sorted.sort_by(|a, b| {
5005 let va = json_field(&a.fields, field);
5006 let vb = json_field(&b.fields, field);
5007 let cmp = compare_json_values(va, vb);
5008 if *descending {
5009 cmp.reverse()
5010 } else {
5011 cmp
5012 }
5013 });
5014 let count = sorted.len();
5015 CalResultPayload::Grains {
5016 grains: sorted,
5017 total_available: Some(count),
5018 }
5019 }
5020
5021 (CalResultPayload::Grains { grains, .. }, PipelineStage::Select { fields, .. }) => {
5023 let projected: Vec<_> = grains
5024 .into_iter()
5025 .map(|g| {
5026 let projected_fields = project_fields(&g.fields, fields);
5027 CalGrainResult {
5028 fields: projected_fields,
5029 ..g
5030 }
5031 })
5032 .collect();
5033 let count = projected.len();
5034 CalResultPayload::Grains {
5035 grains: projected,
5036 total_available: Some(count),
5037 }
5038 }
5039
5040 (
5042 CalResultPayload::Grains { grains, .. },
5043 PipelineStage::Project { fields, .. },
5044 ) => {
5045 let field_names: Vec<String> =
5046 fields.iter().map(|pf| pf.field.clone()).collect();
5047 let projected: Vec<_> = grains
5048 .into_iter()
5049 .map(|g| {
5050 let mut new_map = serde_json::Map::new();
5051 if let serde_json::Value::Object(map) = &g.fields {
5052 for pf in fields {
5053 if let Some(v) = map.get(&pf.field) {
5054 let key = pf.alias.as_deref().unwrap_or(&pf.field);
5055 new_map.insert(key.to_string(), v.clone());
5056 }
5057 }
5058 }
5059 let _ = field_names.len(); CalGrainResult {
5061 fields: serde_json::Value::Object(new_map),
5062 ..g
5063 }
5064 })
5065 .collect();
5066 let count = projected.len();
5067 CalResultPayload::Grains {
5068 grains: projected,
5069 total_available: Some(count),
5070 }
5071 }
5072
5073 (CalResultPayload::Grains { grains, .. }, PipelineStage::Subjects { .. }) => {
5075 let extracted: Vec<CalGrainResult> = grains
5076 .iter()
5077 .filter_map(|g| {
5078 json_field(&g.fields, "subject").map(|v| CalGrainResult {
5079 hash: String::new(),
5080 grain_type: "extracted".into(),
5081 score: 0.0,
5082 fields: serde_json::json!({ "value": v }),
5083 score_breakdown: None,
5084 explanation: None,
5085 relative_time: None,
5086 is_deterministic: true,
5087 contested_by: None,
5088 })
5089 })
5090 .collect();
5091 let count = extracted.len();
5092 CalResultPayload::Grains {
5093 grains: extracted,
5094 total_available: Some(count),
5095 }
5096 }
5097
5098 (CalResultPayload::Grains { grains, .. }, PipelineStage::Objects { .. }) => {
5100 let extracted: Vec<CalGrainResult> = grains
5101 .iter()
5102 .filter_map(|g| {
5103 json_field(&g.fields, "object").map(|v| CalGrainResult {
5104 hash: String::new(),
5105 grain_type: "extracted".into(),
5106 score: 0.0,
5107 fields: serde_json::json!({ "value": v }),
5108 score_breakdown: None,
5109 explanation: None,
5110 relative_time: None,
5111 is_deterministic: true,
5112 contested_by: None,
5113 })
5114 })
5115 .collect();
5116 let count = extracted.len();
5117 CalResultPayload::Grains {
5118 grains: extracted,
5119 total_available: Some(count),
5120 }
5121 }
5122
5123 (CalResultPayload::Grains { grains, .. }, PipelineStage::Hashes { .. }) => {
5125 let extracted: Vec<CalGrainResult> = grains
5126 .iter()
5127 .map(|g| CalGrainResult {
5128 hash: String::new(),
5129 grain_type: "extracted".into(),
5130 score: 0.0,
5131 fields: serde_json::json!({ "value": g.hash }),
5132 score_breakdown: None,
5133 explanation: None,
5134 relative_time: None,
5135 is_deterministic: true,
5136 contested_by: None,
5137 })
5138 .collect();
5139 let count = extracted.len();
5140 CalResultPayload::Grains {
5141 grains: extracted,
5142 total_available: Some(count),
5143 }
5144 }
5145
5146 (
5150 CalResultPayload::Grains {
5151 grains,
5152 total_available,
5153 },
5154 PipelineStage::GroupBy { field, .. },
5155 ) => {
5156 grouped_by = Some(field.clone());
5157 let grouped = group_grains_by_field(grains, field);
5158 CalResultPayload::Grains {
5159 grains: grouped,
5160 total_available,
5161 }
5162 }
5163
5164 (
5166 CalResultPayload::Grains { grains, .. },
5167 PipelineStage::Filter { condition, .. },
5168 ) => {
5169 let filtered: Vec<_> = grains
5170 .into_iter()
5171 .filter(|grain| grain_matches_condition_tree(grain, condition))
5172 .collect();
5173 let count = filtered.len();
5174 CalResultPayload::Grains {
5175 grains: filtered,
5176 total_available: Some(count),
5177 }
5178 }
5179
5180 (other, stage) => {
5192 exec_warnings.push(
5193 super::errors::CalWarning::PipelineStageInert {
5194 stage: pipeline_stage_name(stage),
5195 payload: payload_kind_name(&other),
5196 why: inert_stage_reason(&other),
5197 }
5198 .to_string(),
5199 );
5200 other
5201 }
5202 };
5203 }
5204
5205 Ok((current, grouped_by))
5206 }
5207}
5208
5209fn diff_grains(
5218 a: &areev_core::format::deserialize::DeserializedGrain,
5219 b: &areev_core::format::deserialize::DeserializedGrain,
5220) -> Vec<super::ast::FieldDiff> {
5221 use std::collections::BTreeSet;
5222
5223 let keys_a: BTreeSet<&str> = a.fields.keys().map(|s| s.as_str()).collect();
5224 let keys_b: BTreeSet<&str> = b.fields.keys().map(|s| s.as_str()).collect();
5225
5226 let mut diffs = Vec::new();
5227
5228 for key in keys_b.difference(&keys_a) {
5230 diffs.push(super::ast::FieldDiff::Added {
5231 field: key.to_string(),
5232 value: b.fields[*key].clone(),
5233 });
5234 }
5235
5236 for key in keys_a.difference(&keys_b) {
5238 diffs.push(super::ast::FieldDiff::Removed {
5239 field: key.to_string(),
5240 value: a.fields[*key].clone(),
5241 });
5242 }
5243
5244 for key in keys_a.intersection(&keys_b) {
5246 if a.fields[*key] != b.fields[*key] {
5247 diffs.push(super::ast::FieldDiff::Changed {
5248 field: key.to_string(),
5249 old: a.fields[*key].clone(),
5250 new: b.fields[*key].clone(),
5251 });
5252 }
5253 }
5254
5255 diffs
5256}
5257
5258#[allow(clippy::vec_init_then_push)]
5260fn build_features_list() -> Vec<&'static str> {
5261 let mut features = Vec::new();
5262 #[cfg(feature = "http")]
5263 features.push("http");
5264 #[cfg(feature = "grpc")]
5265 features.push("grpc");
5266 #[cfg(feature = "mcp")]
5267 features.push("mcp");
5268 #[cfg(feature = "a2a")]
5269 features.push("a2a");
5270 #[cfg(feature = "app")]
5271 features.push("app");
5272 #[cfg(feature = "signing")]
5273 features.push("signing");
5274 #[cfg(feature = "import")]
5275 features.push("import");
5276 #[cfg(feature = "auth")]
5277 features.push("auth");
5278 #[cfg(feature = "rerank")]
5279 features.push("rerank");
5280 #[cfg(feature = "llm-rerank")]
5281 features.push("llm-rerank");
5282 #[cfg(feature = "pii_ner")]
5283 features.push("pii_ner");
5284 #[cfg(feature = "cal")]
5285 features.push("cal");
5286 features
5287}
5288
5289fn compute_query_hash(input: &str) -> String {
5291 use unicode_normalization::UnicodeNormalization as _;
5292 let normalized: String = input.trim().nfc().collect();
5293 let mut hasher = Sha256::new();
5294 hasher.update(normalized.as_bytes());
5295 hex::encode(hasher.finalize())
5296}
5297
5298pub(super) fn map_store_err(e: AreevError, span: Option<super::errors::Span>) -> CalError {
5304 match e {
5305 AreevError::CryptoError(_) => CalError::CryptoError {
5306 detail: e.to_string(),
5307 span,
5308 },
5309 AreevError::Validation(_) => CalError::InvalidQuery {
5312 detail: e.to_string(),
5313 span,
5314 },
5315 _ => CalError::BudgetExceeded {
5316 detail: e.to_string(),
5317 span,
5318 },
5319 }
5320}
5321
5322fn build_add_options(opts: &[AddWithOption], warnings: &mut Vec<String>) -> AddOptions {
5324 let mut options = AddOptions::default();
5325 for opt in opts {
5326 match opt {
5327 AddWithOption::ExtractEventDate => {
5328 options.extract_event_date = Some(true);
5329 }
5330 AddWithOption::AutoRelate => {
5331 warnings.push(
5339 "CAL-W013: WITH auto_relate is accepted but not implemented — no relations \
5340 are inferred. Link grains explicitly with related_to, or use WITH \
5341 multi_hop(n) on recall to widen through the entity graph."
5342 .to_string(),
5343 );
5344 options.auto_relate = Some(true);
5345 }
5346 AddWithOption::ExtractMemories => {
5347 }
5349 AddWithOption::Sync => {
5350 options.sync = Some(true);
5351 }
5352 AddWithOption::Occurrence => {}
5355 }
5356 }
5357 options
5358}
5359
5360fn value_to_cal_literal(value: &super::ast::Value) -> String {
5362 match value {
5363 super::ast::Value::String { value } => {
5364 format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
5365 }
5366 super::ast::Value::Number { value } => value.to_string(),
5367 super::ast::Value::Boolean { value } => value.to_string(),
5368 super::ast::Value::Hash { value } => format!("#{value}"),
5369 super::ast::Value::Parameter { name } => format!("${name}"),
5370 super::ast::Value::Array { values } => {
5371 let items: Vec<String> = values.iter().map(value_to_cal_literal).collect();
5372 format!("[{}]", items.join(", "))
5373 }
5374 }
5375}
5376
5377fn cal_value_to_json(val: &super::ast::Value) -> serde_json::Value {
5379 match val {
5380 super::ast::Value::String { value } => serde_json::Value::String(value.clone()),
5381 super::ast::Value::Number { value } => {
5388 if value.fract() == 0.0 && value.abs() < 9.007_199_254_740_992e15 {
5389 serde_json::Value::Number(serde_json::Number::from(*value as i64))
5390 } else {
5391 serde_json::Number::from_f64(*value)
5392 .map(serde_json::Value::Number)
5393 .unwrap_or(serde_json::Value::Null)
5394 }
5395 }
5396 super::ast::Value::Boolean { value } => serde_json::Value::Bool(*value),
5397 super::ast::Value::Array { values } => {
5398 serde_json::Value::Array(values.iter().map(cal_value_to_json).collect())
5399 }
5400 super::ast::Value::Hash { value } => serde_json::Value::String(value.clone()),
5401 super::ast::Value::Parameter { name } => serde_json::Value::String(format!("${}", name)),
5402 }
5403}
5404
5405fn governance_unwired(statement: &str) -> CalResultPayload {
5407 CalResultPayload::Unsupported {
5408 statement: statement.into(),
5409 message: "governance is not wired on this surface — the host attaches a \
5410 GovernanceHost (CalExecutor::with_governance) to back RUN LOOP, \
5411 APPROVE, REJECT, APPLY, ROLLBACK, and the loop DESCRIBE reads"
5412 .into(),
5413 }
5414}
5415
5416fn statement_type_name(stmt: &CalStatement) -> String {
5418 match stmt {
5419 CalStatement::Recall(_) => "recall",
5420 CalStatement::SetOp(_) => "set_op",
5421 CalStatement::Exists(_) => "exists",
5422 CalStatement::Assemble(_) => "assemble",
5423 CalStatement::History(_) => "history",
5424 CalStatement::Explain(_) => "explain",
5425 CalStatement::Describe(_) => "describe",
5426 CalStatement::Batch(_) => "batch",
5427 CalStatement::Coalesce(_) => "coalesce",
5428 CalStatement::Add(_) => "add",
5429 CalStatement::AddWorkflow(_) => "add_workflow",
5430 CalStatement::Supersede(_) => "supersede",
5431 CalStatement::SupersedeWorkflow(_) => "supersede_workflow",
5432 CalStatement::Accumulate(_) => "accumulate",
5433 CalStatement::Revert(_) => "revert",
5434 CalStatement::Forget(_) => "forget",
5435 CalStatement::Purge(_) => "purge",
5436 CalStatement::ReportSubject(_) => "report_subject",
5437 CalStatement::DefineTemplate(_) => "define_template",
5438 CalStatement::DropTemplate(_) => "drop_template",
5439 CalStatement::DefineQuery(_) => "define_query",
5440 CalStatement::DropQuery(_) => "drop_query",
5441 CalStatement::RunQuery(_) => "run_query",
5442 CalStatement::Grant(_) => "grant",
5443 CalStatement::Revoke(_) => "revoke",
5444 CalStatement::ShowGrants(_) => "show_grants",
5445 CalStatement::Approve(_) => "approve",
5446 CalStatement::Reject(_) => "reject",
5447 CalStatement::ApplyRec(_) => "apply",
5448 CalStatement::RollbackRec(_) => "rollback",
5449 CalStatement::RunLoop(_) => "run_loop",
5450 CalStatement::Remember(_) => "remember",
5451 CalStatement::EntityAt(_) => "entity_at",
5452 CalStatement::RunTrace(_) => "run_trace",
5453 CalStatement::RunsTouching(_) => "runs_touching",
5454 CalStatement::DerivedFrom(_) => "derived_from",
5455 CalStatement::ShowForks(_) => "show_forks",
5456 CalStatement::Merge(_) => "merge",
5457 CalStatement::Related(_) => "related",
5458 CalStatement::Novelty(_) => "novelty",
5459 }
5460 .to_string()
5461}
5462
5463fn required_scope_for_statement(stmt: &CalStatement) -> &'static str {
5467 match stmt {
5468 CalStatement::Recall(_)
5469 | CalStatement::SetOp(_)
5470 | CalStatement::Exists(_)
5471 | CalStatement::Assemble(_)
5472 | CalStatement::History(_)
5473 | CalStatement::Explain(_)
5474 | CalStatement::Describe(_)
5475 | CalStatement::Batch(_)
5476 | CalStatement::Coalesce(_)
5477 | CalStatement::RunQuery(_)
5478 | CalStatement::ShowGrants(_)
5479 | CalStatement::EntityAt(_)
5480 | CalStatement::RunTrace(_)
5481 | CalStatement::RunsTouching(_)
5482 | CalStatement::DerivedFrom(_)
5483 | CalStatement::ShowForks(_)
5484 | CalStatement::Related(_)
5485 | CalStatement::Novelty(_)
5486 | CalStatement::ReportSubject(_) => "read",
5487
5488 CalStatement::Add(_)
5489 | CalStatement::AddWorkflow(_)
5490 | CalStatement::Supersede(_)
5491 | CalStatement::SupersedeWorkflow(_)
5492 | CalStatement::Accumulate(_)
5493 | CalStatement::Revert(_)
5494 | CalStatement::Remember(_)
5495 | CalStatement::Merge(_) => "write",
5496
5497 CalStatement::Forget(_)
5498 | CalStatement::Purge(_)
5499 | CalStatement::DefineTemplate(_)
5500 | CalStatement::DropTemplate(_)
5501 | CalStatement::DefineQuery(_)
5502 | CalStatement::DropQuery(_)
5503 | CalStatement::Grant(_)
5504 | CalStatement::Revoke(_)
5505 | CalStatement::Approve(_)
5506 | CalStatement::Reject(_)
5507 | CalStatement::ApplyRec(_)
5508 | CalStatement::RollbackRec(_)
5509 | CalStatement::RunLoop(_) => "admin",
5510 }
5511}
5512
5513fn payload_kind_name(payload: &CalResultPayload) -> &'static str {
5515 match payload {
5516 CalResultPayload::Assembled { .. } => "assembled",
5517 CalResultPayload::Count { .. } => "count",
5518 CalResultPayload::Formatted { .. } => "formatted",
5519 CalResultPayload::Exists { .. } => "exists",
5520 CalResultPayload::History { .. } => "history",
5521 CalResultPayload::Explain { .. } => "explain",
5522 CalResultPayload::Batch { .. } => "batch",
5523 _ => "non-grain",
5524 }
5525}
5526
5527fn inert_stage_reason(payload: &CalResultPayload) -> &'static str {
5530 match payload {
5531 CalResultPayload::Assembled { .. } => {
5535 "an assembly is a list of named sections, not a flat grain list — \
5536 section order is FROM-clause order (PRIORITY weights the budget, \
5537 it does not reorder); to order WITHIN a section, put the stage on \
5538 that source's sub-query"
5539 }
5540 CalResultPayload::Count { .. } => "a count is a scalar; stage it before | COUNT",
5541 CalResultPayload::Formatted { .. } => {
5542 "FORMAT has already rendered the grains to text; stage it before FORMAT"
5543 }
5544 _ => "this statement does not return a grain list",
5545 }
5546}
5547
5548fn pipeline_stage_name(stage: &PipelineStage) -> String {
5550 match stage {
5551 PipelineStage::Select { .. } => "SELECT".to_string(),
5552 PipelineStage::OrderBy {
5553 field, descending, ..
5554 } => {
5555 format!(
5556 "ORDER BY {} {}",
5557 field,
5558 if *descending { "DESC" } else { "ASC" }
5559 )
5560 }
5561 PipelineStage::Limit { value, .. } => format!("LIMIT {}", value),
5562 PipelineStage::Offset { value, .. } => format!("OFFSET {}", value),
5563 PipelineStage::Count { .. } => "COUNT".to_string(),
5564 PipelineStage::First { .. } => "FIRST".to_string(),
5565 PipelineStage::Subjects { .. } => "SUBJECTS".to_string(),
5566 PipelineStage::Objects { .. } => "OBJECTS".to_string(),
5567 PipelineStage::Hashes { .. } => "HASHES".to_string(),
5568 PipelineStage::GroupBy { field, .. } => format!("GROUP BY {}", field),
5569 PipelineStage::Project { .. } => "PROJECT".to_string(),
5570 PipelineStage::Filter { .. } => "WHERE (post-pipeline)".to_string(),
5571 }
5572}
5573
5574fn count_payload_results(payload: &CalResultPayload) -> usize {
5576 match payload {
5577 CalResultPayload::Grains { grains, .. } => grains.len(),
5578 CalResultPayload::Exists { .. } => 1,
5579 CalResultPayload::Count { .. } => 1,
5580 CalResultPayload::History { versions } => versions.len(),
5581 CalResultPayload::Describe { .. } => 1,
5582 CalResultPayload::Explain { .. } => 1,
5583 CalResultPayload::Batch { results } => results.len(),
5584 CalResultPayload::Assembled { grains, .. } => grains.len(),
5585 CalResultPayload::Diff { changes, .. } => changes.len(),
5586 CalResultPayload::Formatted { grain_count, .. } => *grain_count,
5587 CalResultPayload::MultiFormatted { grain_count, .. } => *grain_count,
5588 CalResultPayload::Added { .. } => 1,
5589 CalResultPayload::Granted { .. } => 1,
5590 CalResultPayload::Revoked { .. } => 1,
5591 CalResultPayload::GrantList { grants } => grants.len(),
5592 CalResultPayload::LoopRan { .. } => 1,
5593 CalResultPayload::Reviewed { .. } => 1,
5594 CalResultPayload::RecApplied { .. } => 1,
5595 CalResultPayload::RecRolledBack { .. } => 1,
5596 CalResultPayload::Remembered { .. } => 1,
5597 CalResultPayload::EntityAt { grain, .. } => usize::from(grain.is_some()),
5598 CalResultPayload::RunTrace { .. } => 1,
5599 CalResultPayload::RunsTouching { runs, .. } => runs.len(),
5600 CalResultPayload::DerivedFrom { grains, .. } => grains.len(),
5601 CalResultPayload::Forks { forks } => forks.len(),
5602 CalResultPayload::Merged { .. } => 1,
5603 CalResultPayload::RelatedEntities { entities, .. } => entities.len(),
5604 CalResultPayload::NoveltyMatches { matches } => matches.len(),
5605 CalResultPayload::Superseded { .. } => 1,
5606 CalResultPayload::Accumulated { .. } => 1,
5607 CalResultPayload::Forgotten { .. } => 1,
5608 CalResultPayload::Purged { count } => *count,
5609 CalResultPayload::SubjectReport { grains, .. } => grains.len(),
5610 CalResultPayload::Unsupported { .. } => 0,
5611 CalResultPayload::TemplateDefined { .. } => 1,
5612 CalResultPayload::TemplateDropped { .. } => 1,
5613 CalResultPayload::QueryDefined { .. } => 1,
5614 CalResultPayload::QueryDropped { .. } => 1,
5615 CalResultPayload::StreamAssemble { .. } => 0,
5616 }
5617}
5618
5619fn extract_grains(payload: CalResultPayload) -> Vec<CalGrainResult> {
5621 match payload {
5622 CalResultPayload::Grains { grains, .. } => grains,
5623 CalResultPayload::Assembled { grains, .. } => grains,
5624 _ => Vec::new(),
5625 }
5626}
5627
5628fn hits_to_grain_results(hits: &[crate::store_types::SearchHit]) -> Vec<CalGrainResult> {
5630 hits.iter()
5631 .map(|hit| CalGrainResult {
5632 hash: hit.hash.to_hex(),
5633 grain_type: hit.grain.grain_type.as_str().to_string(),
5634 score: hit.score,
5635 fields: serde_json::Value::Object({
5636 let mut map: serde_json::Map<String, serde_json::Value> = hit
5637 .grain
5638 .fields
5639 .iter()
5640 .map(|(k, v)| (k.clone(), v.clone()))
5641 .collect();
5642 if let Some(sup) = hit.superseded_by_hash {
5649 map.entry("superseded_by".to_string())
5650 .or_insert_with(|| serde_json::Value::String(sup.to_hex()));
5651 }
5652 map
5653 }),
5654 score_breakdown: hit
5655 .score_breakdown
5656 .as_ref()
5657 .map(|sb| serde_json::to_value(sb).unwrap_or(serde_json::Value::Null)),
5658 explanation: hit.explanation.clone(),
5659 relative_time: hit.relative_time.clone(),
5660 is_deterministic: false,
5661 contested_by: None,
5662 })
5663 .collect()
5664}
5665
5666fn apply_fork_status(
5683 grains: &mut Vec<CalGrainResult>,
5684 store: &dyn CalStoreFacade,
5685 filter: bool,
5686 restrict_to: Option<&std::collections::HashSet<(String, String, String)>>,
5687 exec_warnings: &mut Vec<String>,
5688) {
5689 let forks = match store.open_forks() {
5690 Ok(f) => f,
5691 Err(e) => {
5692 exec_warnings.push(format!(
5693 "contradiction detection unavailable ({e}); fork status not applied"
5694 ));
5695 if filter {
5696 grains.clear();
5697 }
5698 return;
5699 }
5700 };
5701
5702 let mut peers: HashMap<String, Vec<String>> = HashMap::new();
5704 for f in &forks {
5705 if let Some(keys) = restrict_to {
5706 if !keys.contains(&(
5707 f.namespace.clone(),
5708 f.subject.clone(),
5709 f.relation.clone(),
5710 )) {
5711 continue;
5712 }
5713 }
5714 for (i, head) in f.heads.iter().enumerate() {
5715 let others: Vec<String> = f
5716 .heads
5717 .iter()
5718 .enumerate()
5719 .filter(|(j, _)| *j != i)
5720 .map(|(_, o)| o.clone())
5721 .collect();
5722 peers.insert(head.clone(), others);
5723 }
5724 }
5725
5726 if filter {
5727 grains.retain(|g| peers.contains_key(&g.hash));
5728 }
5729 for g in grains.iter_mut() {
5730 if let Some(others) = peers.get(&g.hash) {
5731 g.contested_by = Some(others.clone());
5732 }
5733 }
5734}
5735
5736fn contradiction_scope_keys(
5749 grains: &[CalGrainResult],
5750 default_ns: Option<&str>,
5751) -> std::collections::HashSet<(String, String, String)> {
5752 grains
5753 .iter()
5754 .filter_map(|g| {
5755 let s = g.fields.get("subject").and_then(|v| v.as_str())?;
5756 let r = g.fields.get("relation").and_then(|v| v.as_str())?;
5757 let ns = g
5758 .fields
5759 .get("namespace")
5760 .and_then(|v| v.as_str())
5761 .or(default_ns)
5762 .unwrap_or_default();
5763 Some((ns.to_string(), s.to_string(), r.to_string()))
5764 })
5765 .collect()
5766}
5767
5768fn grain_result_text(grain: &CalGrainResult) -> String {
5773 if grain.grain_type == "fact" {
5775 let s = grain
5776 .fields
5777 .get("subject")
5778 .and_then(|v| v.as_str())
5779 .unwrap_or("");
5780 let r = grain
5781 .fields
5782 .get("relation")
5783 .and_then(|v| v.as_str())
5784 .unwrap_or("");
5785 let o = grain
5786 .fields
5787 .get("object")
5788 .and_then(|v| v.as_str())
5789 .unwrap_or("");
5790 return format!("{} {} {}", s, r, o).trim().to_string();
5791 }
5792 for field in &[
5794 "content",
5795 "description",
5796 "title",
5797 "goal",
5798 "query",
5799 "result",
5800 "output",
5801 ] {
5802 if let Some(v) = grain.fields.get(*field).and_then(|v| v.as_str()) {
5803 if !v.trim().is_empty() {
5804 return v.to_string();
5805 }
5806 }
5807 }
5808 grain.grain_type.clone()
5809}
5810
5811fn text_similarity(a: &str, b: &str) -> f64 {
5818 if a == b {
5819 return 1.0;
5820 }
5821 let words_a: Vec<&str> = a.split_whitespace().collect();
5822 let words_b: Vec<&str> = b.split_whitespace().collect();
5823 if words_a.is_empty() && words_b.is_empty() {
5824 return 1.0;
5825 }
5826 if words_a.is_empty() || words_b.is_empty() {
5827 return 0.0;
5828 }
5829 let bigrams = |words: &[&str]| -> std::collections::HashSet<(String, String)> {
5831 if words.len() < 2 {
5832 let mut s = std::collections::HashSet::new();
5833 s.insert((words[0].to_lowercase(), String::new()));
5834 return s;
5835 }
5836 words
5837 .windows(2)
5838 .map(|w| (w[0].to_lowercase(), w[1].to_lowercase()))
5839 .collect()
5840 };
5841 let set_a = bigrams(&words_a);
5842 let set_b = bigrams(&words_b);
5843 let intersection = set_a.intersection(&set_b).count();
5844 let union = set_a.union(&set_b).count();
5845 if union == 0 {
5846 return 1.0;
5847 }
5848 intersection as f64 / union as f64
5849}
5850
5851fn value_to_string(value: &Value) -> std::result::Result<String, CalError> {
5859 match value {
5860 Value::String { value } => Ok(value.clone()),
5861 Value::Hash { value } => Ok(value.clone()),
5862 Value::Parameter { name } => Err(CalError::UnboundParameter {
5863 name: name.clone(),
5864 span: None,
5865 }),
5866 other => Err(CalError::IncompatibleTypes {
5867 left: "string".into(),
5868 right: format!("{:?}", other),
5869 span: None,
5870 suggestion: Some("expected a quoted string value".into()),
5871 }),
5872 }
5873}
5874
5875fn value_to_f64(value: &Value) -> std::result::Result<f64, CalError> {
5877 match value {
5878 Value::Number { value } => Ok(*value),
5879 other => Err(CalError::IncompatibleTypes {
5880 left: "number".into(),
5881 right: format!("{:?}", other),
5882 span: None,
5883 suggestion: Some("expected a numeric value".into()),
5884 }),
5885 }
5886}
5887
5888fn extract_hash_from_condition(condition: Option<&Condition>) -> Option<String> {
5890 match condition? {
5891 Condition::Comparison {
5892 field,
5893 comparator: Comparator::Eq,
5894 value: Value::Hash { value },
5895 ..
5896 } if field == "hash" => Some(value.clone()),
5897 _ => None,
5898 }
5899}
5900
5901#[derive(Clone, Copy)]
5916struct RenderInputs<'a> {
5917 user_vars: &'a HashMap<String, String>,
5919 store: &'a dyn CalStoreFacade,
5921 disclosure: Option<crate::render::Disclosure>,
5924}
5925
5926fn disclosure_of(opts: &[super::ast::WithOption]) -> Option<crate::render::Disclosure> {
5933 opts.iter().find_map(|o| match o {
5934 super::ast::WithOption::ProgressiveDisclosure { level } => Some(match level.as_deref() {
5935 Some("summary") => crate::render::Disclosure::Summary,
5936 Some("headlines") => crate::render::Disclosure::Headlines,
5937 _ => crate::render::Disclosure::Full,
5938 }),
5939 _ => None,
5940 })
5941}
5942
5943fn apply_format_clause(
5944 payload: CalResultPayload,
5945 format: &Option<FormatClause>,
5946 grouped_by: Option<&str>,
5947 inputs: RenderInputs<'_>,
5948 assemble_ident: Option<(&str, &str)>,
5951 warnings: &mut Vec<String>,
5952) -> std::result::Result<CalResultPayload, CalError> {
5953 let Some(clause) = format else {
5954 return Ok(payload);
5955 };
5956
5957 let (grains, assembled) = match &payload {
5962 CalResultPayload::Grains { grains, .. } => (grains, None),
5963 CalResultPayload::Assembled {
5964 grains,
5965 sources,
5966 total_tokens,
5967 budget_limit,
5968 ..
5969 } => (grains, Some((sources, *total_tokens, *budget_limit))),
5970 _ => return Ok(payload),
5972 };
5973
5974 let mut render_sources: Vec<super::templates::RenderSource<'_>> = Vec::new();
5975 let mut assembly_ctx: Option<super::templates::AssemblyContext> = None;
5976
5977 if let Some((metas, total_tokens, budget_limit)) = assembled {
5978 let mut offset = 0usize;
5981 for (i, m) in metas.iter().enumerate() {
5982 let end = (offset + m.grain_count).min(grains.len());
5983 render_sources.push(super::templates::RenderSource {
5984 label: &m.label,
5985 grains: &grains[offset..end],
5986 omitted: &m.omitted,
5987 priority: i + 1,
5988 tokens_used: m.tokens_used as usize,
5989 truncated: !m.omitted.is_empty(),
5990 });
5991 offset = end;
5992 }
5993 assembly_ctx = Some(super::templates::AssemblyContext {
5994 name: assemble_ident.map(|(n, _)| n.to_string()).unwrap_or_default(),
5995 intent: assemble_ident.map(|(_, i)| i.to_string()).unwrap_or_default(),
5996 source_count: metas.len(),
5997 grain_count: grains.len(),
5998 budget_total: budget_limit.unwrap_or(0) as u64,
5999 budget_used: total_tokens as u64,
6000 budget_unit: "tokens".to_string(),
6001 });
6002 }
6003
6004 let plan = super::templates::RenderPlan {
6005 assembly: assembly_ctx.as_ref(),
6006 sources: (!render_sources.is_empty()).then_some(render_sources.as_slice()),
6007 };
6008
6009 apply_format_clause_to_grains(
6010 grains, clause, grouped_by, inputs, &plan, warnings,
6011 )
6012}
6013
6014fn apply_format_clause_to_grains(
6024 grains: &[CalGrainResult],
6025 clause: &FormatClause,
6026 grouped_by: Option<&str>,
6027 inputs: RenderInputs<'_>,
6028 plan: &super::templates::RenderPlan<'_>,
6029 warnings: &mut Vec<String>,
6030) -> std::result::Result<CalResultPayload, CalError> {
6031 match clause {
6032 FormatClause::Single(super::ast::FormatSpec::Json) if grouped_by.is_none() => {
6033 Ok(CalResultPayload::Grains {
6037 grains: grains.to_vec(),
6038 total_available: Some(grains.len()),
6039 })
6040 }
6041 FormatClause::Single(spec) => {
6042 format_grain_results(
6043 grains, spec, grouped_by, inputs, plan, warnings,
6044 )
6045 }
6046 FormatClause::Multi(entries) => {
6047 let mut formats = HashMap::new();
6048 for entry in entries {
6049 let rendered =
6050 format_grain_results(
6051 grains, &entry.spec, grouped_by, inputs, plan, warnings,
6052 )?;
6053 if let CalResultPayload::Formatted { text, format, .. } = rendered {
6054 let key = entry.alias.clone().unwrap_or(format);
6055 formats.insert(key, text);
6056 }
6057 }
6058 Ok(CalResultPayload::MultiFormatted {
6059 formats,
6060 grain_count: grains.len(),
6061 grains: grains.to_vec(),
6062 })
6063 }
6064 }
6065}
6066
6067fn grain_view(grain: &CalGrainResult) -> crate::render::GrainView<'_> {
6079 crate::render::GrainView {
6080 grain_type: &grain.grain_type,
6081 hash: &grain.hash,
6082 fields: &grain.fields,
6083 created_at_sec: crate::render::created_at_sec_from_fields(&grain.fields),
6084 }
6085}
6086
6087fn template_tier(
6091 plan: &super::templates::RenderPlan<'_>,
6092 grain_count: usize,
6093) -> super::templates::DisclosureTier {
6094 match plan.assembly {
6095 Some(a) if a.budget_unit == "tokens" && a.budget_total > 0 => {
6096 super::templates::select_tier(a.budget_total.min(u32::MAX as u64) as u32, grain_count)
6097 }
6098 _ => super::templates::DisclosureTier::Full,
6099 }
6100}
6101
6102fn format_grain_results(
6103 grains: &[CalGrainResult],
6104 format: &super::ast::FormatSpec,
6105 grouped_by: Option<&str>,
6106 inputs: RenderInputs<'_>,
6107 plan: &super::templates::RenderPlan<'_>,
6108 warnings: &mut Vec<String>,
6109) -> std::result::Result<CalResultPayload, CalError> {
6110 let RenderInputs { user_vars, store, disclosure } = inputs;
6111 let (text, format_name) = match format {
6112 super::ast::FormatSpec::Json => {
6113 if let Some(field) = grouped_by {
6114 let groups = collect_groups(grains, field);
6115 let json_groups: Vec<serde_json::Value> = groups
6116 .iter()
6117 .map(|(key, members)| {
6118 serde_json::json!({
6119 "group_key": key,
6120 "count": members.len(),
6121 "grains": members,
6122 })
6123 })
6124 .collect();
6125 let json = serde_json::to_string_pretty(&json_groups).unwrap_or_default();
6126 (json, "json")
6127 } else {
6128 let json = serde_json::to_string_pretty(grains).unwrap_or_default();
6129 (json, "json")
6130 }
6131 }
6132 super::ast::FormatSpec::Markdown => {
6133 let mut md = String::new();
6134 if let Some(field) = grouped_by {
6135 let groups = collect_groups(grains, field);
6136 for (key, members) in &groups {
6137 md.push_str(&format!(
6138 "### {} ({} {})\n\n",
6139 key,
6140 members.len(),
6141 if members.len() == 1 {
6142 "memory"
6143 } else {
6144 "memories"
6145 }
6146 ));
6147 for grain in members {
6148 md.push_str(&crate::render::render_grain_markdown_at(
6149 &grain_view(grain),
6150 disclosure,
6151 ));
6152 md.push('\n');
6153 }
6154 }
6155 } else {
6156 for grain in grains {
6160 md.push_str(&crate::render::render_grain_markdown_at(
6161 &grain_view(grain),
6162 disclosure,
6163 ));
6164 md.push('\n');
6165 }
6166 }
6167 (md, "markdown")
6168 }
6169 super::ast::FormatSpec::Yaml => {
6170 let mut yaml = String::new();
6172 for (i, grain) in grains.iter().enumerate() {
6173 yaml.push_str(&format!("- hash: \"{}\"\n", grain.hash));
6174 yaml.push_str(&format!(" grain_type: \"{}\"\n", grain.grain_type));
6175 if let serde_json::Value::Object(map) = &grain.fields {
6176 yaml.push_str(" fields:\n");
6177 for (k, v) in map {
6178 yaml.push_str(&format!(" {}: {}\n", k, v));
6179 }
6180 }
6181 if i < grains.len() - 1 {
6182 yaml.push('\n');
6183 }
6184 }
6185 (yaml, "yaml")
6186 }
6187 super::ast::FormatSpec::Text => {
6188 let mut text = String::new();
6189 if let Some(field) = grouped_by {
6190 let groups = collect_groups(grains, field);
6191 let total_groups = groups.len();
6192 for (idx, (key, members)) in groups.iter().enumerate() {
6193 text.push_str(&format!(
6194 "--- Group {}/{}: {} ({} {}) ---\n",
6195 idx + 1,
6196 total_groups,
6197 key,
6198 members.len(),
6199 if members.len() == 1 {
6200 "memory"
6201 } else {
6202 "memories"
6203 }
6204 ));
6205 for (i, grain) in members.iter().enumerate() {
6206 text.push_str(&crate::render::render_grain_text_line(
6207 &grain_view(grain),
6208 Some(i + 1),
6209 ));
6210 text.push('\n');
6211 }
6212 if idx + 1 < total_groups {
6213 text.push('\n');
6214 }
6215 }
6216 } else {
6217 for grain in grains {
6218 text.push_str(&crate::render::render_grain_text_line(
6219 &grain_view(grain),
6220 None,
6221 ));
6222 text.push('\n');
6223 }
6224 }
6225 (text, "text")
6226 }
6227 super::ast::FormatSpec::Sml => {
6228 let level = crate::render::MetadataDetail::Minimal;
6231 let mut sml = String::from("<grains>\n");
6232 if let Some(field) = grouped_by {
6233 let groups = collect_groups(grains, field);
6234 for (key, members) in &groups {
6235 let escaped_key = crate::render::sml_escape(key);
6236 sml.push_str(&format!(
6237 " <group key=\"{}\" count=\"{}\">\n",
6238 escaped_key,
6239 members.len()
6240 ));
6241 for grain in members {
6242 sml.push_str(" ");
6243 sml.push_str(&crate::render::render_grain_sml_at(
6244 &grain_view(grain),
6245 level,
6246 disclosure,
6247 ));
6248 sml.push('\n');
6249 }
6250 sml.push_str(" </group>\n");
6251 }
6252 } else {
6253 for grain in grains {
6254 sml.push_str(" ");
6255 sml.push_str(&crate::render::render_grain_sml_at(
6256 &grain_view(grain),
6257 level,
6258 disclosure,
6259 ));
6260 sml.push('\n');
6261 }
6262 }
6263 sml.push_str("</grains>");
6264 (sml, "sml")
6265 }
6266 super::ast::FormatSpec::Toon => {
6267 let views: Vec<crate::render::GrainView<'_>> = grains.iter().map(grain_view).collect();
6268 (crate::render::render_toon(&views), "toon")
6269 }
6270 super::ast::FormatSpec::Triples => {
6271 let mut triples = String::new();
6272 for grain in grains {
6273 if let serde_json::Value::Object(map) = &grain.fields {
6274 let s = map.get("subject").and_then(|v| v.as_str()).unwrap_or("_");
6275 let r = map.get("relation").and_then(|v| v.as_str()).unwrap_or("_");
6276 let o = map.get("object").and_then(|v| v.as_str()).unwrap_or("_");
6277 triples.push_str(&format!("{}\t{}\t{}\n", s, r, o));
6278 }
6279 }
6280 (triples, "triples")
6281 }
6282 super::ast::FormatSpec::Csv => {
6283 let mut csv = String::from("hash,grain_type,subject,relation,object,confidence\n");
6284 for grain in grains {
6285 let get_field = |f: &str| -> String {
6286 json_field(&grain.fields, f)
6287 .map(|v| match v {
6288 serde_json::Value::String(s) => {
6289 if s.contains(',') || s.contains('"') || s.contains('\n') {
6291 format!("\"{}\"", s.replace('"', "\"\""))
6292 } else {
6293 s.clone()
6294 }
6295 }
6296 _ => v.to_string(),
6297 })
6298 .unwrap_or_default()
6299 };
6300 csv.push_str(&format!(
6301 "{},{},{},{},{},{}\n",
6302 grain.hash,
6303 grain.grain_type,
6304 get_field("subject"),
6305 get_field("relation"),
6306 get_field("object"),
6307 get_field("confidence"),
6308 ));
6309 }
6310 (csv, "csv")
6311 }
6312 super::ast::FormatSpec::Table => {
6313 let header = "| hash | grain_type | subject | relation | object | confidence |";
6314 let separator = "| --- | --- | --- | --- | --- | --- |";
6315 let mut table = format!("{}\n{}\n", header, separator);
6316 for grain in grains {
6317 let get_field = |f: &str| -> String {
6318 json_field(&grain.fields, f)
6319 .map(|v| match v {
6320 serde_json::Value::String(s) => s.replace('|', "\\|"),
6321 _ => v.to_string(),
6322 })
6323 .unwrap_or_default()
6324 };
6325 table.push_str(&format!(
6326 "| {} | {} | {} | {} | {} | {} |\n",
6327 &grain.hash[..8.min(grain.hash.len())],
6328 grain.grain_type,
6329 get_field("subject"),
6330 get_field("relation"),
6331 get_field("object"),
6332 get_field("confidence"),
6333 ));
6334 }
6335 (table, "table")
6336 }
6337 super::ast::FormatSpec::Preset { name } | super::ast::FormatSpec::TemplateRef { name } => {
6340 let info = store
6342 .get_template(name)
6343 .ok_or_else(|| CalError::TemplateNotFound {
6344 name: name.clone(),
6345 span: None,
6346 })?;
6347 let mut parsed = super::templates::parse_template_any(&info.source)?;
6352
6353 let parent_name = info.parent.clone().or_else(|| {
6358 (parsed.is_sectioned() && !info.builtin).then(|| "readable".to_string())
6359 });
6360 if let Some(parent_name) = parent_name {
6361 if let Some(parent_info) = store.get_template(&parent_name) {
6362 let parent = super::templates::parse_template_any(&parent_info.source)?;
6363 parsed = super::templates::merge_templates(&parent, &parsed);
6364 }
6365 }
6366 let user_vars_map: std::collections::HashMap<String, String> = user_vars
6367 .iter()
6368 .map(|(k, v)| (k.clone(), v.clone()))
6369 .collect();
6370 let ctx = super::templates::RenderContext {
6371 now_secs: std::time::SystemTime::now()
6372 .duration_since(std::time::UNIX_EPOCH)
6373 .map(|d| d.as_secs() as i64)
6374 .unwrap_or(0),
6375 tier: template_tier(plan, grains.len()),
6376 total_count: grains.len(),
6377 user_vars: user_vars_map,
6378 };
6379 let rendered = match parsed.sections() {
6380 Some(sections) => super::templates::render_sectioned(
6381 sections,
6382 plan.sources
6383 .unwrap_or(&[super::templates::RenderSource::single(grains)]),
6384 &ctx,
6385 plan.assembly,
6386 super::templates::MAX_RENDER_OUTPUT_SIZE,
6387 )?,
6388 None => {
6389 if let Some((rendered, total)) =
6392 super::templates::each_iteration_cap(&parsed, grains.len())
6393 {
6394 warnings.push(
6395 super::errors::CalWarning::EachIterationCapped {
6396 rendered,
6397 total,
6398 max: super::templates::MAX_EACH_ITERATIONS,
6399 }
6400 .to_string(),
6401 );
6402 }
6403 super::templates::render(&parsed, grains, &ctx)?
6404 }
6405 };
6406 store.record_template_run(name);
6407 let key = match format {
6408 super::ast::FormatSpec::Preset { .. } => "preset",
6409 _ => "template",
6410 };
6411 (rendered, key)
6412 }
6413 super::ast::FormatSpec::Template { template } => {
6414 let user_vars_map: std::collections::HashMap<String, String> = user_vars
6421 .iter()
6422 .map(|(k, v)| (k.clone(), v.clone()))
6423 .collect();
6424 let ctx = super::templates::RenderContext {
6425 now_secs: std::time::SystemTime::now()
6426 .duration_since(std::time::UNIX_EPOCH)
6427 .map(|d| d.as_secs() as i64)
6428 .unwrap_or(0),
6429 tier: template_tier(plan, grains.len()),
6430 total_count: grains.len(),
6431 user_vars: user_vars_map,
6432 };
6433 let sections = super::templates::TemplateSections {
6434 element: Some(super::templates::parse_section(template)?),
6435 ..Default::default()
6436 };
6437 let rendered = super::templates::render_sectioned(
6438 §ions,
6439 plan.sources
6440 .unwrap_or(&[super::templates::RenderSource::single(grains)]),
6441 &ctx,
6442 plan.assembly,
6443 super::templates::MAX_RENDER_OUTPUT_SIZE,
6444 )?;
6445 (rendered, "template")
6446 }
6447 super::ast::FormatSpec::TemplateInline { sections } => {
6448 let user_vars_map: std::collections::HashMap<String, String> = user_vars
6452 .iter()
6453 .map(|(k, v)| (k.clone(), v.clone()))
6454 .collect();
6455 let ctx = super::templates::RenderContext {
6456 now_secs: std::time::SystemTime::now()
6457 .duration_since(std::time::UNIX_EPOCH)
6458 .map(|d| d.as_secs() as i64)
6459 .unwrap_or(0),
6460 tier: template_tier(plan, grains.len()),
6461 total_count: grains.len(),
6462 user_vars: user_vars_map,
6463 };
6464 let parsed = super::templates::parse_sections(sections)?;
6465 let rendered = super::templates::render_sectioned(
6466 &parsed,
6467 plan.sources
6468 .unwrap_or(&[super::templates::RenderSource::single(grains)]),
6469 &ctx,
6470 plan.assembly,
6471 super::templates::MAX_RENDER_OUTPUT_SIZE,
6472 )?;
6473 (rendered, "template")
6474 }
6475 };
6476
6477 Ok(CalResultPayload::Formatted {
6478 text,
6479 format: format_name.to_string(),
6480 grain_count: grains.len(),
6481 grains: grains.to_vec(),
6482 })
6483}
6484
6485fn group_grains_by_field(grains: Vec<CalGrainResult>, field: &str) -> Vec<CalGrainResult> {
6493 let mut groups: BTreeMap<String, Vec<CalGrainResult>> = BTreeMap::new();
6495 for grain in grains {
6496 let key = json_field(&grain.fields, field)
6497 .map(|v| match v {
6498 serde_json::Value::String(s) => s.clone(),
6499 _ => v.to_string(),
6500 })
6501 .unwrap_or_default();
6502 groups.entry(key).or_default().push(grain);
6503 }
6504
6505 for members in groups.values_mut() {
6507 members.sort_by(|a, b| {
6508 let ta = json_field(&a.fields, "created_at_sec").and_then(|v| v.as_f64());
6509 let tb = json_field(&b.fields, "created_at_sec").and_then(|v| v.as_f64());
6510 ta.partial_cmp(&tb).unwrap_or(Ordering::Equal)
6511 });
6512 }
6513
6514 let mut group_vec: Vec<(String, Vec<CalGrainResult>)> = groups.into_iter().collect();
6516 group_vec.sort_by(|a, b| {
6517 let earliest_a =
6518 a.1.first()
6519 .and_then(|g| json_field(&g.fields, "created_at_sec").and_then(|v| v.as_f64()));
6520 let earliest_b =
6521 b.1.first()
6522 .and_then(|g| json_field(&g.fields, "created_at_sec").and_then(|v| v.as_f64()));
6523 earliest_a
6524 .partial_cmp(&earliest_b)
6525 .unwrap_or(Ordering::Equal)
6526 });
6527
6528 if let Some(pos) = group_vec.iter().position(|(k, _)| k.is_empty()) {
6530 let empty = group_vec.remove(pos);
6531 group_vec.push(empty);
6532 }
6533
6534 group_vec
6536 .into_iter()
6537 .flat_map(|(_, members)| members)
6538 .collect()
6539}
6540
6541fn collect_groups<'a>(
6544 grains: &'a [CalGrainResult],
6545 field: &str,
6546) -> Vec<(String, Vec<&'a CalGrainResult>)> {
6547 let mut groups: Vec<(String, Vec<&CalGrainResult>)> = Vec::new();
6548 for grain in grains {
6549 let key = json_field(&grain.fields, field)
6550 .map(|v| match v {
6551 serde_json::Value::String(s) => s.clone(),
6552 _ => v.to_string(),
6553 })
6554 .unwrap_or_default();
6555 if let Some(last) = groups.last_mut() {
6556 if last.0 == key {
6557 last.1.push(grain);
6558 continue;
6559 }
6560 }
6561 groups.push((key, vec![grain]));
6562 }
6563 groups
6564}
6565
6566const COMMON_FIELDS: &[&str] = &[
6577 "subject",
6579 "relation",
6580 "object",
6581 "namespace",
6582 "user_id",
6583 "confidence",
6584 "importance",
6585 "score",
6586 "tags",
6587 "type",
6588 "time",
6589 "hash",
6590 "contradicted",
6591 "verification_status",
6592 "source_type",
6593 "recall_priority",
6594 "epistemic_status",
6595 "query",
6596 "created_at",
6598 "content",
6599 "summary",
6600 "grain_type",
6601 "scope",
6602 "scope_path",
6603 "priority",
6604 "status",
6605];
6606
6607fn type_specific_fields(grain_type: &GrainTypePlural) -> &'static [&'static str] {
6615 match grain_type.to_grain_type() {
6618 Some(ty) => areev_core::types::registry::meta(ty).queryable_fields,
6619 None => &[],
6620 }
6621}
6622
6623fn is_known_type_specific_field(field: &str) -> bool {
6625 areev_core::types::registry::GRAIN_TYPES
6627 .iter()
6628 .any(|m| m.queryable_fields.contains(&field))
6629}
6630
6631fn suggest_field(field: &str, grain_type: &GrainTypePlural) -> Option<String> {
6633 let valid_fields = type_specific_fields(grain_type);
6634 for known in valid_fields {
6635 if known.contains(field) || field.contains(known) {
6637 return Some(known.to_string());
6638 }
6639 }
6640 if is_known_type_specific_field(field) {
6642 return Some(format!(
6643 "'{}' exists on a different grain type, not on {}",
6644 field,
6645 grain_type.as_str()
6646 ));
6647 }
6648 None
6649}
6650
6651const ENGINE_ONLY_FIELDS: &[&str] = &[
6662 "query",
6663 "time",
6664 "entity",
6665 "contradicted",
6666 "scope",
6667 "scope_path",
6668 "tags",
6669];
6670
6671const GRAIN_EVALUABLE_COMMON: &[&str] = &[
6676 "subject",
6677 "relation",
6678 "object",
6679 "namespace",
6680 "user_id",
6681 "confidence",
6682 "importance",
6683 "created_at",
6684 "verification_status",
6685 "source_type",
6686 "recall_priority",
6687 "epistemic_status",
6688 "content",
6689 "summary",
6690 "hash",
6691 "grain_type",
6692 "type",
6693 "score",
6694];
6695
6696fn leaf_pushdown_consumed(condition: &Condition) -> bool {
6709 match condition {
6710 Condition::Comparison {
6711 field, comparator, ..
6712 } => matches!(
6713 (field.as_str(), comparator),
6714 (
6715 "subject" | "relation" | "object" | "namespace" | "user_id" | "query" | "time"
6716 | "entity" | "scope" | "scope_path",
6717 Comparator::Eq
6718 ) | ("confidence" | "importance", Comparator::Gte | Comparator::Gt)
6719 | ("contradicted", Comparator::Eq)
6720 ),
6721 Condition::In { field, .. } => matches!(
6722 field.as_str(),
6723 "subject" | "relation" | "object" | "tags" | "namespace"
6724 ),
6725 Condition::NotIn { field, .. } => field == "tags",
6726 Condition::Contains { field, .. } => {
6727 matches!(field.as_str(), "subject" | "object" | "content" | "summary")
6728 }
6729 Condition::IsCategory { .. } => true,
6733 _ => false,
6734 }
6735}
6736
6737fn plan_residual_where(
6757 condition: &Condition,
6758 grain_type: &GrainTypePlural,
6759 warnings: &mut Vec<String>,
6760) -> std::result::Result<Option<Condition>, CalError> {
6761 match condition {
6762 Condition::And { left, right, span } => {
6763 let l = plan_residual_where(left, grain_type, warnings)?;
6764 let r = plan_residual_where(right, grain_type, warnings)?;
6765 Ok(match (l, r) {
6766 (None, None) => None,
6767 (Some(c), None) | (None, Some(c)) => Some(c),
6768 (Some(a), Some(b)) => Some(Condition::And {
6769 left: Box::new(a),
6770 right: Box::new(b),
6771 span: *span,
6772 }),
6773 })
6774 }
6775 Condition::Or { left, right, .. } => {
6776 validate_residual_subtree(left, grain_type, "under NOT/OR", warnings)?;
6777 validate_residual_subtree(right, grain_type, "under NOT/OR", warnings)?;
6778 Ok(Some(condition.clone()))
6779 }
6780 Condition::Not { inner, .. } => {
6781 validate_residual_subtree(inner, grain_type, "under NOT/OR", warnings)?;
6782 Ok(Some(condition.clone()))
6783 }
6784 leaf => {
6785 if leaf_pushdown_consumed(leaf) {
6786 Ok(None)
6787 } else {
6788 validate_residual_leaf(leaf, grain_type, None, warnings)?;
6789 Ok(Some(leaf.clone()))
6790 }
6791 }
6792 }
6793}
6794
6795fn validate_residual_subtree(
6798 condition: &Condition,
6799 grain_type: &GrainTypePlural,
6800 context: &str,
6801 warnings: &mut Vec<String>,
6802) -> std::result::Result<(), CalError> {
6803 match condition {
6804 Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
6805 validate_residual_subtree(left, grain_type, context, warnings)?;
6806 validate_residual_subtree(right, grain_type, context, warnings)
6807 }
6808 Condition::Not { inner, .. } => {
6809 validate_residual_subtree(inner, grain_type, context, warnings)
6810 }
6811 leaf => validate_residual_leaf(leaf, grain_type, Some(context), warnings),
6812 }
6813}
6814
6815fn validate_residual_leaf(
6818 leaf: &Condition,
6819 grain_type: &GrainTypePlural,
6820 forced_context: Option<&str>,
6821 warnings: &mut Vec<String>,
6822) -> std::result::Result<(), CalError> {
6823 let (field, span, own_context): (&str, Option<Span>, String) = match leaf {
6824 Condition::Comparison {
6825 field,
6826 comparator,
6827 span,
6828 ..
6829 } => (field, *span, format!("with comparator {comparator}")),
6830 Condition::In { field, span, .. } => (field, *span, "with IN".to_string()),
6831 Condition::NotIn { field, span, .. } => (field, *span, "with NOT IN".to_string()),
6832 Condition::Contains { field, span, .. } => (field, *span, "with CONTAINS".to_string()),
6833 Condition::StartsWith { field, span, .. } => {
6834 (field, *span, "with STARTS WITH".to_string())
6835 }
6836 Condition::IsNull { field, span, .. } | Condition::IsNotNull { field, span, .. } => {
6837 (field, *span, "with IS [NOT] NULL".to_string())
6838 }
6839 Condition::IsCategory { field, span, .. } => (field, *span, "with IS".to_string()),
6840 _ => return Ok(()),
6842 };
6843
6844 if field.contains(':') {
6847 return Ok(());
6848 }
6849 if ENGINE_ONLY_FIELDS.contains(&field) {
6850 return Err(CalError::EngineFieldNotFilterable {
6851 field: field.to_string(),
6852 context: forced_context.map(str::to_string).unwrap_or(own_context),
6853 span,
6854 });
6855 }
6856 if GRAIN_EVALUABLE_COMMON.contains(&field) {
6857 return Ok(());
6858 }
6859 if grain_type.to_grain_type().is_some() {
6860 if type_specific_fields(grain_type).contains(&field) {
6861 return Ok(());
6862 }
6863 let suggestion = suggest_field(field, grain_type);
6864 return Err(CalError::FieldNotOnGrainType {
6865 field: field.to_string(),
6866 grain_type: grain_type.as_str().to_string(),
6867 span,
6868 suggestion,
6869 });
6870 }
6871 if !is_known_type_specific_field(field) {
6875 warnings.push(
6876 super::errors::CalWarning::UnrecognizedWhereField {
6877 field: field.to_string(),
6878 span,
6879 }
6880 .to_string(),
6881 );
6882 }
6883 Ok(())
6884}
6885
6886pub fn grain_matches_condition(
6890 grain: &CalGrainResult,
6891 field: &str,
6892 comparator: &Comparator,
6893 value: &Value,
6894) -> bool {
6895 let envelope: Option<serde_json::Value> = match field {
6901 "hash" => Some(serde_json::Value::String(grain.hash.clone())),
6902 "grain_type" | "type" => Some(serde_json::Value::String(grain.grain_type.clone())),
6904 "score" => serde_json::Number::from_f64(grain.score).map(serde_json::Value::Number),
6906 "kind" if grain.grain_type == "tool" && json_field(&grain.fields, "kind").is_none() => {
6912 Some(serde_json::Value::String("execution".into()))
6913 }
6914 "status" if grain.grain_type == "tool" && json_field(&grain.fields, "status").is_none() => {
6915 Some(serde_json::Value::String("completed".into()))
6916 }
6917 _ => None,
6918 };
6919 let grain_value = match &envelope {
6920 Some(v) => Some(v),
6921 None => json_field(&grain.fields, field),
6922 };
6923 let value = &match (field, value) {
6924 ("hash", Value::String { value: v }) => Value::String {
6925 value: v.strip_prefix("sha256:").unwrap_or(v).to_string(),
6926 },
6927 _ => value.clone(),
6928 };
6929
6930 match comparator {
6931 Comparator::Eq => match value {
6932 Value::String { value: target } => grain_value
6933 .and_then(|v| v.as_str())
6934 .map(|s| s == target.as_str())
6935 .unwrap_or(false),
6936 Value::Number { value: target } => grain_value
6937 .and_then(|v| v.as_f64())
6938 .map(|n| (n - target).abs() < f64::EPSILON)
6939 .unwrap_or(false),
6940 Value::Boolean { value: target } => grain_value
6941 .and_then(|v| v.as_bool())
6942 .map(|b| b == *target)
6943 .unwrap_or(false),
6944 _ => false,
6945 },
6946 Comparator::NotEq => !grain_matches_condition(grain, field, &Comparator::Eq, value),
6947 Comparator::Gte => match value {
6948 Value::Number { value: target } => grain_value
6949 .and_then(|v| v.as_f64())
6950 .map(|n| n >= *target)
6951 .unwrap_or(false),
6952 _ => false,
6953 },
6954 Comparator::Gt => match value {
6955 Value::Number { value: target } => grain_value
6956 .and_then(|v| v.as_f64())
6957 .map(|n| n > *target)
6958 .unwrap_or(false),
6959 _ => false,
6960 },
6961 Comparator::Lte => match value {
6962 Value::Number { value: target } => grain_value
6963 .and_then(|v| v.as_f64())
6964 .map(|n| n <= *target)
6965 .unwrap_or(false),
6966 _ => false,
6967 },
6968 Comparator::Lt => match value {
6969 Value::Number { value: target } => grain_value
6970 .and_then(|v| v.as_f64())
6971 .map(|n| n < *target)
6972 .unwrap_or(false),
6973 _ => false,
6974 },
6975 }
6976}
6977
6978pub fn grain_matches_condition_tree(grain: &CalGrainResult, condition: &Condition) -> bool {
6997 match condition {
6998 Condition::Comparison {
6999 field,
7000 comparator,
7001 value,
7002 ..
7003 } => grain_matches_condition(grain, field, comparator, value),
7004 Condition::And { left, right, .. } => {
7005 grain_matches_condition_tree(grain, left) && grain_matches_condition_tree(grain, right)
7006 }
7007 Condition::Or { left, right, .. } => {
7008 grain_matches_condition_tree(grain, left) || grain_matches_condition_tree(grain, right)
7009 }
7010 Condition::Not { inner, .. } => !grain_matches_condition_tree(grain, inner),
7011 Condition::In { field, values, .. } => values
7012 .iter()
7013 .any(|v| grain_matches_condition(grain, field, &Comparator::Eq, v)),
7014 Condition::NotIn { field, values, .. } => !values
7015 .iter()
7016 .any(|v| grain_matches_condition(grain, field, &Comparator::Eq, v)),
7017 Condition::IsNull { field, .. } => {
7018 json_field(&grain.fields, field).is_none()
7019 || json_field(&grain.fields, field) == Some(&serde_json::Value::Null)
7020 }
7021 Condition::IsNotNull { field, .. } => {
7022 matches!(json_field(&grain.fields, field), Some(v) if !v.is_null())
7023 }
7024 Condition::Contains { field, value, .. } => json_field(&grain.fields, field)
7025 .and_then(|v| v.as_str())
7026 .map(|s| s.contains(value.as_str()))
7027 .unwrap_or(false),
7028 Condition::StartsWith { field, value, .. } => json_field(&grain.fields, field)
7029 .and_then(|v| v.as_str())
7030 .map(|s| s.starts_with(value.as_str()))
7031 .unwrap_or(false),
7032 Condition::IsCategory {
7033 field, category, ..
7034 } => json_field(&grain.fields, field)
7035 .and_then(|v| v.as_str())
7036 .map(|s| s.eq_ignore_ascii_case(category))
7037 .unwrap_or(false),
7038 }
7039}
7040
7041fn collect_filter_names(condition: &Condition, names: &mut Vec<String>) {
7044 match condition {
7045 Condition::Comparison { field, .. } => {
7046 if !names.contains(field) {
7047 names.push(field.clone());
7048 }
7049 }
7050 Condition::In { field, .. } | Condition::NotIn { field, .. } => {
7051 if !names.contains(field) {
7052 names.push(field.clone());
7053 }
7054 }
7055 Condition::IsNull { field, .. } | Condition::IsNotNull { field, .. } => {
7056 if !names.contains(field) {
7057 names.push(field.clone());
7058 }
7059 }
7060 Condition::Contains { field, .. } | Condition::StartsWith { field, .. } => {
7061 if !names.contains(field) {
7062 names.push(field.clone());
7063 }
7064 }
7065 Condition::IsCategory { field, .. } => {
7066 if !names.contains(field) {
7067 names.push(field.clone());
7068 }
7069 }
7070 Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
7071 collect_filter_names(left, names);
7072 collect_filter_names(right, names);
7073 }
7074 Condition::Not { inner, .. } => {
7075 collect_filter_names(inner, names);
7076 }
7077 }
7078}
7079
7080fn value_dedup_key(v: &serde_json::Value) -> String {
7085 match v {
7086 serde_json::Value::String(s) => s.clone(),
7087 other => other.to_string(),
7088 }
7089}
7090
7091fn json_field<'a>(fields: &'a serde_json::Value, field: &str) -> Option<&'a serde_json::Value> {
7092 if let serde_json::Value::Object(map) = fields {
7093 map.get(field)
7094 } else {
7095 None
7096 }
7097}
7098
7099fn project_fields(fields: &serde_json::Value, selected: &[String]) -> serde_json::Value {
7101 let mut out = serde_json::Map::new();
7102 if let serde_json::Value::Object(map) = fields {
7103 for key in selected {
7104 if let Some(v) = map.get(key) {
7105 out.insert(key.clone(), v.clone());
7106 }
7107 }
7108 }
7109 serde_json::Value::Object(out)
7110}
7111
7112fn compare_json_values(a: Option<&serde_json::Value>, b: Option<&serde_json::Value>) -> Ordering {
7114 match (a, b) {
7115 (None, None) => Ordering::Equal,
7116 (None, Some(_)) => Ordering::Less,
7117 (Some(_), None) => Ordering::Greater,
7118 (Some(va), Some(vb)) => {
7119 if let (Some(fa), Some(fb)) = (va.as_f64(), vb.as_f64()) {
7121 fa.partial_cmp(&fb).unwrap_or(Ordering::Equal)
7122 } else if let (Some(sa), Some(sb)) = (va.as_str(), vb.as_str()) {
7123 sa.cmp(sb)
7124 } else if let (Some(ba), Some(bb)) = (va.as_bool(), vb.as_bool()) {
7125 ba.cmp(&bb)
7126 } else {
7127 va.to_string().cmp(&vb.to_string())
7129 }
7130 }
7131 }
7132}
7133
7134fn union_grains(mut left: Vec<CalGrainResult>, right: Vec<CalGrainResult>) -> Vec<CalGrainResult> {
7136 let left_hashes: std::collections::HashSet<String> =
7137 left.iter().map(|g| g.hash.clone()).collect();
7138 for g in right {
7139 if !left_hashes.contains(&g.hash) {
7140 left.push(g);
7141 }
7142 }
7143 left
7144}
7145
7146fn intersect_grains(left: Vec<CalGrainResult>, right: &[CalGrainResult]) -> Vec<CalGrainResult> {
7148 let right_hashes: std::collections::HashSet<&str> =
7149 right.iter().map(|g| g.hash.as_str()).collect();
7150 left.into_iter()
7151 .filter(|g| right_hashes.contains(g.hash.as_str()))
7152 .collect()
7153}
7154
7155fn except_grains(left: Vec<CalGrainResult>, right: &[CalGrainResult]) -> Vec<CalGrainResult> {
7157 let right_hashes: std::collections::HashSet<&str> =
7158 right.iter().map(|g| g.hash.as_str()).collect();
7159 left.into_iter()
7160 .filter(|g| !right_hashes.contains(g.hash.as_str()))
7161 .collect()
7162}
7163
7164#[cfg(test)]
7169mod tests {
7170 use super::*;
7171 use crate::facade::CalStoreFacade;
7172 use crate::store_types::{RecallParams, SearchHit};
7173 use crate::store_types::VersionEntry;
7174 use areev_core::error::{AreevError, Hash};
7175 use areev_core::format::deserialize::DeserializedGrain;
7176 use areev_core::format::header::MgHeader;
7177 use areev_core::types::GrainType;
7178 use std::collections::HashMap;
7179
7180 struct MockStore {
7185 grains: Vec<(Hash, DeserializedGrain)>,
7186 }
7187
7188 impl MockStore {
7189 fn empty() -> Self {
7190 Self { grains: Vec::new() }
7191 }
7192
7193 fn with_grains(grains: Vec<(Hash, DeserializedGrain)>) -> Self {
7194 Self { grains }
7195 }
7196 }
7197
7198 fn make_fact(subject: &str, relation: &str, object: &str) -> (Hash, DeserializedGrain) {
7200 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
7201 fields.insert("subject".into(), serde_json::json!(subject));
7202 fields.insert("relation".into(), serde_json::json!(relation));
7203 fields.insert("object".into(), serde_json::json!(object));
7204 fields.insert("grain_type".into(), serde_json::json!("fact"));
7205 fields.insert("confidence".into(), serde_json::json!(0.9));
7206
7207 let mut hash_bytes = [0u8; 32];
7209 let key = format!("{}|{}|{}", subject, relation, object);
7210 for (i, b) in key.as_bytes().iter().enumerate().take(32) {
7211 hash_bytes[i] = *b;
7212 }
7213 let hash = Hash::from_bytes(&hash_bytes);
7214
7215 let grain = DeserializedGrain {
7216 header: MgHeader {
7217 version: 1,
7218 flags: 0,
7219 grain_type: 0x01,
7220 ns_hash: 0,
7221 created_at_sec: 0,
7222 },
7223 grain_type: GrainType::Fact,
7224 fields,
7225 hash,
7226 };
7227 (hash, grain)
7228 }
7229
7230 impl CalStoreFacade for MockStore {
7231 fn recall(&self, params: &RecallParams) -> areev_core::error::Result<Vec<SearchHit>> {
7232 let mut hits: Vec<SearchHit> = self
7233 .grains
7234 .iter()
7235 .filter(|(_, g)| {
7236 if let Some(ref s) = params.subject {
7237 if g.get_str("subject") != Some(s.as_str()) {
7238 return false;
7239 }
7240 }
7241 if let Some(ref r) = params.relation {
7242 if g.get_str("relation") != Some(r.as_str()) {
7243 return false;
7244 }
7245 }
7246 true
7247 })
7248 .map(|(hash, grain)| SearchHit {
7249 grain: grain.clone(),
7250 score: 1.0,
7251 hash: *hash,
7252 score_breakdown: None,
7253 explanation: None,
7254 scope_depth: None,
7255 source_namespace: None,
7256 #[cfg(feature = "rerank")]
7257 rerank_score: None,
7258 #[cfg(feature = "llm-rerank")]
7259 llm_rerank_score: None,
7260 relative_time: None,
7261 conflict_status: None,
7262 supersession_status: None,
7263 superseded_by_hash: None,
7264 recall_source: None,
7265 })
7266 .collect();
7267 if let Some(limit) = params.limit {
7268 hits.truncate(limit);
7269 }
7270 Ok(hits)
7271 }
7272
7273 fn exists(&self, hash: &Hash) -> areev_core::error::Result<bool> {
7274 Ok(self.grains.iter().any(|(h, _)| h == hash))
7275 }
7276
7277 fn get(&self, hash: &Hash) -> areev_core::error::Result<DeserializedGrain> {
7278 self.grains
7279 .iter()
7280 .find(|(h, _)| h == hash)
7281 .map(|(_, g)| g.clone())
7282 .ok_or(AreevError::NotFound(*hash))
7283 }
7284
7285 fn count(&self) -> areev_core::error::Result<usize> {
7286 Ok(self.grains.len())
7287 }
7288
7289 fn get_history(
7290 &self,
7291 _ns: &str,
7292 _s: &str,
7293 _r: &str,
7294 ) -> areev_core::error::Result<Vec<VersionEntry>> {
7295 Ok(Vec::new())
7296 }
7297
7298 fn default_namespace(&self) -> Option<&str> {
7299 None
7300 }
7301
7302 fn active_user(&self) -> Option<&str> {
7303 None
7304 }
7305
7306 fn cal_add(
7307 &self,
7308 _grain_type: &str,
7309 _fields: &serde_json::Map<String, serde_json::Value>,
7310 ) -> areev_core::error::Result<Hash> {
7311 Err(AreevError::Validation(
7312 "mock: cal_add not implemented".into(),
7313 ))
7314 }
7315
7316 fn cal_supersede(
7317 &self,
7318 _old_hash: &Hash,
7319 _grain_type: &str,
7320 _fields: &serde_json::Map<String, serde_json::Value>,
7321 ) -> areev_core::error::Result<Hash> {
7322 Err(AreevError::Validation(
7323 "mock: cal_supersede not implemented".into(),
7324 ))
7325 }
7326
7327 fn list_templates(&self) -> Vec<crate::facade::TemplateInfo> {
7328 let registry = crate::templates::TemplateRegistry::new();
7329 registry.list()
7330 }
7331 }
7332
7333 fn exec() -> CalExecutor {
7338 CalExecutor::with_defaults()
7339 }
7340
7341 #[test]
7346 fn test_execute_recall_empty_store() {
7347 let store = MockStore::empty();
7348 let ex = exec();
7349 let result = ex.execute("RECALL facts", &store).unwrap();
7350 assert_eq!(result.metadata.statement_type, "recall");
7351 match result.result {
7352 CalResultPayload::Grains { grains, .. } => assert!(grains.is_empty()),
7353 other => panic!("unexpected payload: {:?}", other),
7354 }
7355 }
7356
7357 #[test]
7362 fn test_execute_recall_where_subject() {
7363 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
7364 let (hash_b, grain_b) = make_fact("bob", "likes", "coffee");
7365 let store = MockStore::with_grains(vec![(hash_a, grain_a), (hash_b, grain_b)]);
7366 let ex = exec();
7367 let result = ex
7368 .execute(r#"RECALL facts WHERE subject = "john""#, &store)
7369 .unwrap();
7370 match result.result {
7371 CalResultPayload::Grains { grains, .. } => {
7372 assert_eq!(grains.len(), 1);
7373 assert_eq!(grains[0].hash, hash_a.to_hex());
7374 }
7375 other => panic!("unexpected payload: {:?}", other),
7376 }
7377 }
7378
7379 #[test]
7384 fn test_execute_recall_about_clause() {
7385 let store = MockStore::empty();
7386 let ex = exec();
7387 let result = ex
7389 .execute(r#"RECALL facts ABOUT "coffee preferences""#, &store)
7390 .unwrap();
7391 assert_eq!(result.metadata.statement_type, "recall");
7392 }
7393
7394 #[test]
7399 fn test_execute_recall_pipeline_limit() {
7400 let grains: Vec<_> = (0..10u8)
7401 .map(|i| {
7402 let key = format!("user{}", i);
7403 make_fact(&key, "likes", "coffee")
7404 })
7405 .collect();
7406 let store = MockStore::with_grains(grains);
7407 let ex = exec();
7408 let result = ex.execute("RECALL facts LIMIT 3", &store).unwrap();
7409 match result.result {
7410 CalResultPayload::Grains { grains, .. } => assert_eq!(grains.len(), 3),
7411 other => panic!("unexpected: {:?}", other),
7412 }
7413 }
7414
7415 #[test]
7420 fn test_execute_recall_pipeline_count() {
7421 let grains: Vec<_> = (0..5u8)
7422 .map(|i| make_fact(&format!("u{}", i), "likes", "tea"))
7423 .collect();
7424 let store = MockStore::with_grains(grains);
7425 let ex = exec();
7426 let result = ex.execute("RECALL facts COUNT", &store).unwrap();
7427 match result.result {
7428 CalResultPayload::Count { count } => assert_eq!(count, 5),
7429 other => panic!("unexpected: {:?}", other),
7430 }
7431 }
7432
7433 #[test]
7438 fn test_execute_recall_pipeline_order_by() {
7439 let mut grains = vec![
7440 make_fact("charlie", "likes", "rust"),
7441 make_fact("john", "likes", "python"),
7442 make_fact("bob", "likes", "go"),
7443 ];
7444 for (i, (_, g)) in grains.iter_mut().enumerate() {
7446 g.fields
7447 .insert("created_at".into(), serde_json::json!(i as i64));
7448 }
7449 let store = MockStore::with_grains(grains);
7450 let ex = exec();
7451 let result = ex.execute("RECALL facts ORDER BY subject", &store).unwrap();
7452 match result.result {
7453 CalResultPayload::Grains { grains, .. } => {
7454 assert_eq!(grains[0].hash, make_fact("bob", "likes", "go").0.to_hex());
7456 assert_eq!(
7457 grains[1].hash,
7458 make_fact("charlie", "likes", "rust").0.to_hex()
7459 );
7460 }
7461 other => panic!("unexpected: {:?}", other),
7462 }
7463 }
7464
7465 #[test]
7470 fn test_execute_recall_pipeline_select() {
7471 let (hash, _) = make_fact("john", "likes", "vim");
7472 let store = MockStore::with_grains(vec![make_fact("john", "likes", "vim")]);
7473 let ex = exec();
7474 let result = ex
7475 .execute("RECALL facts SELECT subject, object", &store)
7476 .unwrap();
7477 match result.result {
7478 CalResultPayload::Grains { grains, .. } => {
7479 assert_eq!(grains.len(), 1);
7480 if let serde_json::Value::Object(map) = &grains[0].fields {
7481 assert!(map.contains_key("subject"));
7482 assert!(map.contains_key("object"));
7483 assert!(!map.contains_key("relation"));
7485 } else {
7486 panic!("expected Object fields");
7487 }
7488 let _ = hash;
7489 }
7490 other => panic!("unexpected: {:?}", other),
7491 }
7492 }
7493
7494 #[test]
7502 fn test_execute_exists_known_hash() {
7503 let (hash, grain) = make_fact("john", "is", "a developer");
7504 let store = MockStore::with_grains(vec![(hash, grain)]);
7505 let ex = exec();
7506 let hex = hash.to_hex();
7507 let query = format!("EXISTS sha256:{}", hex);
7509 let result = ex.execute(&query, &store).unwrap();
7510 match result.result {
7511 CalResultPayload::Exists { exists, .. } => assert!(exists),
7512 other => panic!("unexpected: {:?}", other),
7513 }
7514 }
7515
7516 #[test]
7523 fn test_execute_exists_unknown_hash() {
7524 let store = MockStore::empty();
7525 let ex = exec();
7526 let hex = "a".repeat(64);
7528 let query = format!("EXISTS sha256:{}", hex);
7529 let result = ex.execute(&query, &store).unwrap();
7530 match result.result {
7531 CalResultPayload::Exists { exists, .. } => assert!(!exists),
7532 other => panic!("unexpected: {:?}", other),
7533 }
7534 }
7535
7536 #[test]
7541 fn test_execute_describe_schema() {
7542 let store = MockStore::empty();
7543 let ex = exec();
7544 let result = ex.execute("DESCRIBE SCHEMA", &store).unwrap();
7545 assert_eq!(result.metadata.statement_type, "describe");
7546 match result.result {
7547 CalResultPayload::Describe { info } => {
7548 assert!(info.get("grain_types").is_some());
7549 assert!(info.get("common_fields").is_some());
7550 }
7551 other => panic!("unexpected: {:?}", other),
7552 }
7553 }
7554
7555 #[test]
7560 fn test_execute_explain_recall() {
7561 let store = MockStore::empty();
7562 let ex = exec();
7563 let result = ex
7564 .execute(r#"EXPLAIN RECALL facts ABOUT "preferences""#, &store)
7565 .unwrap();
7566 assert_eq!(result.metadata.statement_type, "explain");
7567 match result.result {
7568 CalResultPayload::Explain { plan } => {
7569 assert_eq!(plan.statement_type, "recall");
7570 assert!(!plan.index_usage.is_empty());
7571 }
7572 other => panic!("unexpected: {:?}", other),
7573 }
7574 }
7575
7576 #[test]
7577 fn test_explain_reports_like_as_the_bm25_leg_it_runs() {
7578 let store = MockStore::empty();
7584 let ex = exec();
7585 let plan_of = |q: &str| match ex.execute(q, &store).unwrap().result {
7586 CalResultPayload::Explain { plan } => plan,
7587 other => panic!("unexpected: {:?}", other),
7588 };
7589
7590 let like = plan_of(r#"EXPLAIN RECALL facts LIKE "window""#);
7591 assert_eq!(like.query_routing, "bm25");
7592 assert!(like.index_usage.contains(&"bm25_fts".to_string()));
7593 assert!(
7594 like.estimated_cost.starts_with("O(log n)"),
7595 "LIKE must not be reported as a full scan, got: {}",
7596 like.estimated_cost
7597 );
7598
7599 let about = plan_of(r#"EXPLAIN RECALL facts ABOUT "window""#);
7600 assert_eq!(like.query_routing, about.query_routing);
7601 assert_eq!(like.index_usage, about.index_usage);
7602
7603 let anchored = plan_of(r#"EXPLAIN RECALL facts LIKE "window" WHERE subject = "john""#);
7605 assert_eq!(anchored.query_routing, "hybrid_rrf");
7606 }
7607
7608 #[test]
7613 fn test_execute_batch_two_queries() {
7614 let store = MockStore::empty();
7615 let ex = exec();
7616 let result = ex
7617 .execute("BATCH { RECALL facts ; RECALL events }", &store)
7618 .unwrap();
7619 assert_eq!(result.metadata.statement_type, "batch");
7620 match result.result {
7621 CalResultPayload::Batch { results } => {
7622 assert_eq!(results.len(), 2);
7623 assert!(results.contains_key("0"));
7624 assert!(results.contains_key("1"));
7625 }
7626 other => panic!("unexpected: {:?}", other),
7627 }
7628 }
7629
7630 #[test]
7638 fn test_execute_coalesce() {
7639 let (hash, grain) = make_fact("john", "likes", "coffee");
7640 let store = MockStore::with_grains(vec![(hash, grain)]);
7641 let ex = exec();
7642 let result = ex
7644 .execute(
7645 r#"COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
7646 &store,
7647 )
7648 .unwrap();
7649 match result.result {
7650 CalResultPayload::Grains { grains, .. } => assert_eq!(grains.len(), 1),
7651 other => panic!("unexpected: {:?}", other),
7652 }
7653 }
7654
7655 #[test]
7660 fn test_execute_coalesce_fallback_to_second_branch() {
7661 let (hash, grain) = make_fact("bob", "likes", "coffee");
7662 let store = MockStore::with_grains(vec![(hash, grain)]);
7663 let ex = exec();
7664 let result = ex
7666 .execute(
7667 r#"COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
7668 &store,
7669 )
7670 .unwrap();
7671 match result.result {
7672 CalResultPayload::Grains { grains, .. } => {
7673 assert_eq!(grains.len(), 1, "should fall back to second branch");
7674 assert_eq!(
7675 grains[0].fields.get("subject").and_then(|v| v.as_str()),
7676 Some("bob"),
7677 "result should be from the second branch (bob)"
7678 );
7679 }
7680 other => panic!("expected Grains, got: {:?}", other),
7681 }
7682 }
7683
7684 #[test]
7689 fn test_execute_coalesce_all_branches_empty() {
7690 let store = MockStore::empty();
7691 let ex = exec();
7692 let result = ex
7693 .execute(
7694 r#"COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
7695 &store,
7696 )
7697 .unwrap();
7698 match result.result {
7699 CalResultPayload::Grains { grains, .. } => {
7700 assert!(grains.is_empty(), "all branches empty → empty result");
7701 }
7702 other => panic!("expected Grains, got: {:?}", other),
7703 }
7704 }
7705
7706 #[test]
7711 fn test_execute_batch_five_queries() {
7712 let (hash, grain) = make_fact("john", "likes", "coffee");
7713 let store = MockStore::with_grains(vec![(hash, grain)]);
7714 let ex = exec();
7715 let result = ex
7716 .execute(
7717 r#"BATCH { RECALL facts ; RECALL events ; RECALL tools ; RECALL goals ; RECALL facts WHERE subject = "john" }"#,
7718 &store,
7719 )
7720 .unwrap();
7721 assert_eq!(result.metadata.statement_type, "batch");
7722 match result.result {
7723 CalResultPayload::Batch { results } => {
7724 assert_eq!(results.len(), 5, "BATCH should execute all 5 sub-queries");
7725 assert!(results.contains_key("0"));
7728 assert!(results.contains_key("4"));
7729 }
7730 other => panic!("expected Batch, got: {:?}", other),
7731 }
7732 }
7733
7734 #[test]
7744 fn test_execute_tier1_add_returns_e044() {
7745 let store = MockStore::empty();
7749 let ex = CalExecutor::new(CalExecutorConfig {
7750 tier1_enabled: false,
7751 ..Default::default()
7752 });
7753 let err = ex
7754 .execute(
7755 r#"ADD fact SET subject = "john" SET relation = "likes" SET object = "rust" REASON "test""#,
7756 &store,
7757 )
7758 .expect_err("Tier 1 disabled must error, not Unsupported-payload");
7759 assert_eq!(err.code(), "CAL-E044");
7760 assert!(err.to_string().contains("ADD"));
7761 }
7762
7763 #[test]
7769 fn test_execute_generic_add_rejects_unshapeable_type() {
7770 let store = MockStore::empty();
7771 let ex = exec();
7772 let result = ex
7773 .execute(r#"ADD event SET content = "test" REASON "test""#, &store)
7774 .unwrap();
7775 match result.result {
7776 CalResultPayload::Unsupported { message, .. } => {
7777 assert!(
7778 message.contains("cannot be created via ADD"),
7779 "expected grain type restriction message, got: {}",
7780 message
7781 );
7782 assert!(
7783 message.contains("event"),
7784 "message should mention the rejected type, got: {}",
7785 message
7786 );
7787 }
7788 other => panic!("expected Unsupported, got: {:?}", other),
7789 }
7790 }
7791
7792 #[test]
7797 fn test_execute_add_unresolved_parameter() {
7798 let store = MockStore::empty();
7799 let ex = exec();
7800 let result = ex
7801 .execute(
7802 r#"ADD fact SET subject = $user SET relation = "likes" SET object = "rust" REASON "test""#,
7803 &store,
7804 )
7805 .unwrap();
7806 match result.result {
7807 CalResultPayload::Unsupported { message, .. } => {
7808 assert!(
7809 message.contains("Unresolved parameter"),
7810 "expected unresolved parameter message, got: {}",
7811 message
7812 );
7813 assert!(
7814 message.contains("$user"),
7815 "message should mention the parameter name, got: {}",
7816 message
7817 );
7818 }
7819 other => panic!("expected Unsupported, got: {:?}", other),
7820 }
7821 }
7822
7823 #[test]
7828 fn test_added_payload_has_extraction_fields() {
7829 let payload = CalResultPayload::Added {
7831 hash: "abc123".into(),
7832 grain_type: "fact".into(),
7833 extracted_count: Some(3),
7834 extraction_warnings: vec!["warn1".into()],
7835 };
7836 match payload {
7837 CalResultPayload::Added {
7838 extracted_count,
7839 extraction_warnings,
7840 ..
7841 } => {
7842 assert_eq!(extracted_count, Some(3));
7843 assert_eq!(extraction_warnings.len(), 1);
7844 }
7845 _ => unreachable!(),
7846 }
7847
7848 let payload2 = CalResultPayload::Added {
7850 hash: "def456".into(),
7851 grain_type: "observation".into(),
7852 extracted_count: None,
7853 extraction_warnings: vec![],
7854 };
7855 match payload2 {
7856 CalResultPayload::Added {
7857 extracted_count,
7858 extraction_warnings,
7859 ..
7860 } => {
7861 assert_eq!(extracted_count, None);
7862 assert!(extraction_warnings.is_empty());
7863 }
7864 _ => unreachable!(),
7865 }
7866 }
7867
7868 #[test]
7873 fn test_execute_recall_with_score_breakdown() {
7874 let store = MockStore::empty();
7875 let ex = exec();
7876 let result = ex
7879 .execute("RECALL facts WITH score_breakdown", &store)
7880 .unwrap();
7881 assert_eq!(result.metadata.statement_type, "recall");
7882 }
7883
7884 #[test]
7889 fn test_query_hash_is_computed_c4() {
7890 let store = MockStore::empty();
7891 let ex = exec();
7892 let input = "RECALL facts";
7893 let result = ex.execute(input, &store).unwrap();
7894 assert_eq!(result.query_hash.len(), 64);
7896 assert!(result.query_hash.chars().all(|c| c.is_ascii_hexdigit()));
7897 let result2 = ex.execute(input, &store).unwrap();
7899 assert_eq!(result.query_hash, result2.query_hash);
7900 }
7901
7902 #[test]
7907 fn test_namespace_override_applied() {
7908 let store = MockStore::empty();
7909 let config = CalExecutorConfig {
7910 namespace_override: Some("tenant_a".to_string()),
7911 ..Default::default()
7912 };
7913 let ex = CalExecutor::new(config);
7914 let result = ex
7918 .execute(r#"RECALL facts WHERE namespace = "other""#, &store)
7919 .unwrap();
7920 assert_eq!(result.metadata.statement_type, "recall");
7921 }
7922
7923 #[test]
7928 fn test_compute_query_hash_stable() {
7929 let h1 = compute_query_hash("RECALL facts");
7930 let h2 = compute_query_hash("RECALL facts");
7931 assert_eq!(h1, h2);
7932 }
7933
7934 #[test]
7939 fn test_compute_query_hash_distinct() {
7940 let h1 = compute_query_hash("RECALL facts");
7941 let h2 = compute_query_hash("RECALL events");
7942 assert_ne!(h1, h2);
7943 }
7944
7945 #[test]
7950 fn test_execute_pipeline_offset() {
7951 let grains: Vec<_> = (0..5u8)
7952 .map(|i| make_fact(&format!("u{}", i), "likes", "rust"))
7953 .collect();
7954 let store = MockStore::with_grains(grains);
7955 let ex = exec();
7956 let result = ex.execute("RECALL facts OFFSET 3", &store).unwrap();
7957 match result.result {
7958 CalResultPayload::Grains { grains, .. } => {
7959 assert_eq!(grains.len(), 2);
7961 }
7962 other => panic!("unexpected: {:?}", other),
7963 }
7964 }
7965
7966 #[test]
7971 fn test_execute_pipeline_first() {
7972 let grains: Vec<_> = (0..5u8)
7973 .map(|i| make_fact(&format!("u{}", i), "likes", "rust"))
7974 .collect();
7975 let store = MockStore::with_grains(grains);
7976 let ex = exec();
7977 let result = ex.execute("RECALL facts FIRST", &store).unwrap();
7978 match result.result {
7979 CalResultPayload::Grains { grains, .. } => assert_eq!(grains.len(), 1),
7980 other => panic!("unexpected: {:?}", other),
7981 }
7982 }
7983
7984 #[test]
7989 fn test_execute_pipeline_hashes() {
7990 let (hash, grain) = make_fact("john", "is", "a developer");
7991 let store = MockStore::with_grains(vec![(hash, grain)]);
7992 let ex = exec();
7993 let result = ex.execute("RECALL facts HASHES", &store).unwrap();
7994 match result.result {
7996 CalResultPayload::Grains { grains, .. } => {
7997 assert_eq!(grains.len(), 1);
7998 assert_eq!(grains[0].grain_type, "extracted");
7999 let value = grains[0].fields.get("value").unwrap();
8000 assert_eq!(value.as_str().unwrap(), hash.to_hex());
8001 }
8002 other => panic!("unexpected: {:?}", other),
8003 }
8004 }
8005
8006 #[test]
8011 fn test_execute_describe_facts() {
8012 let store = MockStore::empty();
8013 let ex = exec();
8014 let result = ex.execute("DESCRIBE facts", &store).unwrap();
8015 match result.result {
8016 CalResultPayload::Describe { info } => {
8017 assert_eq!(info["grain_type"], "facts");
8018 assert!(info.get("specific_fields").is_some());
8019 }
8020 other => panic!("unexpected: {:?}", other),
8021 }
8022 }
8023
8024 #[test]
8029 fn test_execute_tier1_supersede_returns_e044() {
8030 let store = MockStore::empty();
8033 let ex = CalExecutor::new(CalExecutorConfig {
8034 tier1_enabled: false,
8035 ..Default::default()
8036 });
8037 let err = ex
8038 .execute(
8039 r#"SUPERSEDE sha256:abc123def456 SET object = "new value" REASON "update""#,
8040 &store,
8041 )
8042 .expect_err("Tier 1 disabled must error, not Unsupported-payload");
8043 assert_eq!(err.code(), "CAL-E044");
8044 assert!(err.to_string().contains("SUPERSEDE"));
8045 }
8046
8047 #[test]
8052 fn test_execute_tier1_revert_returns_unsupported_s2() {
8053 let store = MockStore::empty();
8054 let ex = exec();
8057 let result = ex
8058 .execute(r#"REVERT sha256:abc123def456 REASON "mistake""#, &store)
8059 .unwrap();
8060 match result.result {
8061 CalResultPayload::Unsupported { message, .. } => {
8062 assert!(
8063 message.contains("Tier 1"),
8064 "REVERT must return Tier 1 unsupported message (S-2), got: {}",
8065 message
8066 );
8067 }
8068 other => panic!("expected Unsupported for REVERT, got: {:?}", other),
8069 }
8070 }
8071
8072 #[test]
8077 fn test_query_hash_format_c4() {
8078 let store = MockStore::empty();
8079 let ex = exec();
8080 let queries = [
8081 "RECALL facts",
8082 "RECALL events",
8083 "EXISTS sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
8084 "DESCRIBE SCHEMA",
8085 "BATCH { RECALL facts ; RECALL events }",
8086 ];
8087 for q in &queries {
8088 let result = ex.execute(q, &store).unwrap();
8089 assert_eq!(
8090 result.query_hash.len(),
8091 64,
8092 "query_hash must be 64 hex chars for '{}', got {}",
8093 q,
8094 result.query_hash.len()
8095 );
8096 assert!(
8097 result.query_hash.chars().all(|c| c.is_ascii_hexdigit()),
8098 "query_hash must be hex for '{}', got '{}'",
8099 q,
8100 result.query_hash
8101 );
8102 }
8103 }
8104
8105 #[test]
8110 fn test_query_hash_nfc_equivalence_c4_s6() {
8111 let store = MockStore::empty();
8112 let ex = exec();
8113 let decomposed = "RECALL facts WHERE subject = \"caf\u{0065}\u{0301}\"";
8115 let precomposed = "RECALL facts WHERE subject = \"caf\u{00E9}\"";
8116 let r1 = ex.execute(decomposed, &store).unwrap();
8117 let r2 = ex.execute(precomposed, &store).unwrap();
8118 assert_eq!(
8119 r1.query_hash, r2.query_hash,
8120 "NFC-equivalent inputs must produce the same query_hash (C-4 + S-6)"
8121 );
8122 }
8123
8124 #[test]
8129 fn test_or_condition_warning_propagation() {
8130 let store = MockStore::empty();
8131 let ex = exec();
8132 let result = ex
8133 .execute(
8134 r#"RECALL facts WHERE subject = "john" OR subject = "bob""#,
8135 &store,
8136 )
8137 .unwrap();
8138 assert_eq!(result.metadata.statement_type, "recall");
8141 }
8142
8143 #[test]
8148 fn test_assemble_where_clause_applied() {
8149 let store = MockStore::empty();
8152 let ex = exec();
8153 let result = ex
8154 .execute(
8155 r#"ASSEMBLE "summary" FROM (RECALL facts) WHERE confidence >= 0.8"#,
8156 &store,
8157 )
8158 .unwrap();
8159 match result.result {
8161 CalResultPayload::Grains { grains, .. } => {
8162 assert!(grains.is_empty(), "empty store should return no grains");
8163 }
8164 other => panic!("expected Grains, got: {:?}", other),
8165 }
8166 }
8167
8168 #[test]
8173 fn test_execute_empty_input_error() {
8174 let store = MockStore::empty();
8175 let ex = exec();
8176 let result = ex.execute("", &store);
8177 assert!(result.is_err(), "empty input must return an error");
8178 }
8179
8180 #[test]
8185 fn test_execute_whitespace_only_error() {
8186 let store = MockStore::empty();
8187 let ex = exec();
8188 let result = ex.execute(" \t\n ", &store);
8189 assert!(
8190 result.is_err(),
8191 "whitespace-only input must return an error"
8192 );
8193 }
8194
8195 #[test]
8200 fn test_execute_query_too_long_error() {
8201 let store = MockStore::empty();
8202 let ex = exec();
8203 let huge = format!("RECALL facts WHERE subject = \"{}\"", "a".repeat(66_000));
8205 let result = ex.execute(&huge, &store);
8206 assert!(result.is_err(), "oversized input must return an error");
8207 }
8208
8209 #[test]
8214 fn test_execute_malformed_inputs_no_panic() {
8215 let store = MockStore::empty();
8216 let ex = exec();
8217 let malformed = [
8218 "RECALL",
8219 "WHERE subject",
8220 "|||",
8221 "RECALL facts |",
8222 "RECALL facts WHERE",
8223 "RECALL facts WHERE subject =",
8224 "RECALL 123",
8225 "; ; ;",
8226 "RECALL facts beliefs beliefs",
8227 "RECALL facts WHERE subject = \"unterminated",
8228 ];
8229 for input in &malformed {
8230 let _ = ex.execute(input, &store);
8232 }
8233 }
8234
8235 #[test]
8240 fn test_history_diff_identical_grains() {
8241 let (hash, grain) = make_fact("john", "likes", "coffee");
8242 let store = MockStore::with_grains(vec![(hash, grain)]);
8243 let ex = exec();
8244
8245 let history_stmt = super::HistoryStmt {
8246 hash: hash.to_hex(),
8247 where_clause: None,
8248 diff_target: Some(hash.to_hex()),
8249 span: None,
8250 };
8251 let query = super::CalQuery {
8252 version: super::super::ast::CalVersion(1),
8253 statement: super::CalStatement::History(history_stmt),
8254 pipeline: Vec::new(),
8255 with_options: Vec::new(),
8256 format: None,
8257 let_bindings: Vec::new(),
8258 let_values: Default::default(),
8259 user_vars: HashMap::new(),
8260 warnings: Vec::new(),
8261 };
8262 let mut warnings = Vec::new();
8263 let payload = ex
8264 .execute_statement(&query.statement, &store, &query, &mut warnings)
8265 .unwrap();
8266
8267 match payload {
8268 CalResultPayload::Diff { changes, .. } => {
8269 assert!(
8270 changes.is_empty(),
8271 "identical grains should have no differences"
8272 );
8273 }
8274 other => panic!("expected Diff, got {:?}", other),
8275 }
8276 assert!(warnings.is_empty(), "no warnings for same subject/relation");
8277 }
8278
8279 #[test]
8280 fn test_history_diff_different_grains() {
8281 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
8282 let (hash_b, grain_b) = make_fact("john", "likes", "tea");
8283 let store = MockStore::with_grains(vec![(hash_a, grain_a), (hash_b, grain_b)]);
8284 let ex = exec();
8285
8286 let history_stmt = super::HistoryStmt {
8287 hash: hash_a.to_hex(),
8288 where_clause: None,
8289 diff_target: Some(hash_b.to_hex()),
8290 span: None,
8291 };
8292 let query = super::CalQuery {
8293 version: super::super::ast::CalVersion(1),
8294 statement: super::CalStatement::History(history_stmt),
8295 pipeline: Vec::new(),
8296 with_options: Vec::new(),
8297 format: None,
8298 let_bindings: Vec::new(),
8299 let_values: Default::default(),
8300 user_vars: HashMap::new(),
8301 warnings: Vec::new(),
8302 };
8303 let mut warnings = Vec::new();
8304 let payload = ex
8305 .execute_statement(&query.statement, &store, &query, &mut warnings)
8306 .unwrap();
8307
8308 match payload {
8309 CalResultPayload::Diff {
8310 source_hash,
8311 target_hash,
8312 changes,
8313 } => {
8314 assert_eq!(source_hash, hash_a.to_hex());
8315 assert_eq!(target_hash, hash_b.to_hex());
8316 let obj_change = changes.iter().find(|c| {
8317 matches!(c,
8318 super::super::ast::FieldDiff::Changed { field, .. } if field == "object"
8319 )
8320 });
8321 assert!(
8322 obj_change.is_some(),
8323 "expected 'object' field to be Changed"
8324 );
8325 }
8326 other => panic!("expected Diff, got {:?}", other),
8327 }
8328 assert!(
8329 !warnings.iter().any(|w| w.contains("CAL-W005")),
8330 "same subject+relation should not trigger CAL-W005"
8331 );
8332 }
8333
8334 #[test]
8335 fn test_history_diff_w005_different_subject() {
8336 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
8337 let (hash_b, grain_b) = make_fact("bob", "likes", "coffee");
8338 let store = MockStore::with_grains(vec![(hash_a, grain_a), (hash_b, grain_b)]);
8339 let ex = exec();
8340
8341 let history_stmt = super::HistoryStmt {
8342 hash: hash_a.to_hex(),
8343 where_clause: None,
8344 diff_target: Some(hash_b.to_hex()),
8345 span: None,
8346 };
8347 let query = super::CalQuery {
8348 version: super::super::ast::CalVersion(1),
8349 statement: super::CalStatement::History(history_stmt),
8350 pipeline: Vec::new(),
8351 with_options: Vec::new(),
8352 format: None,
8353 let_bindings: Vec::new(),
8354 let_values: Default::default(),
8355 user_vars: HashMap::new(),
8356 warnings: Vec::new(),
8357 };
8358 let mut warnings = Vec::new();
8359 let _ = ex
8360 .execute_statement(&query.statement, &store, &query, &mut warnings)
8361 .unwrap();
8362 assert!(
8363 warnings.iter().any(|w| w.contains("CAL-W005")),
8364 "different subject/relation should trigger CAL-W005, got: {:?}",
8365 warnings
8366 );
8367 }
8368
8369 #[test]
8370 fn test_history_diff_source_not_found() {
8371 let (hash_b, grain_b) = make_fact("john", "likes", "coffee");
8372 let store = MockStore::with_grains(vec![(hash_b, grain_b)]);
8373 let ex = exec();
8374
8375 let fake_hash = "a".repeat(64);
8376 let history_stmt = super::HistoryStmt {
8377 hash: fake_hash,
8378 where_clause: None,
8379 diff_target: Some(hash_b.to_hex()),
8380 span: None,
8381 };
8382 let query = super::CalQuery {
8383 version: super::super::ast::CalVersion(1),
8384 statement: super::CalStatement::History(history_stmt),
8385 pipeline: Vec::new(),
8386 with_options: Vec::new(),
8387 format: None,
8388 let_bindings: Vec::new(),
8389 let_values: Default::default(),
8390 user_vars: HashMap::new(),
8391 warnings: Vec::new(),
8392 };
8393 let mut warnings = Vec::new();
8394 let result = ex.execute_statement(&query.statement, &store, &query, &mut warnings);
8395 assert!(result.is_err(), "should error when source grain not found");
8396 }
8397
8398 #[test]
8399 fn test_history_diff_target_not_found() {
8400 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
8401 let store = MockStore::with_grains(vec![(hash_a, grain_a)]);
8402 let ex = exec();
8403
8404 let fake_hash = "b".repeat(64);
8405 let history_stmt = super::HistoryStmt {
8406 hash: hash_a.to_hex(),
8407 where_clause: None,
8408 diff_target: Some(fake_hash),
8409 span: None,
8410 };
8411 let query = super::CalQuery {
8412 version: super::super::ast::CalVersion(1),
8413 statement: super::CalStatement::History(history_stmt),
8414 pipeline: Vec::new(),
8415 with_options: Vec::new(),
8416 format: None,
8417 let_bindings: Vec::new(),
8418 let_values: Default::default(),
8419 user_vars: HashMap::new(),
8420 warnings: Vec::new(),
8421 };
8422 let mut warnings = Vec::new();
8423 let result = ex.execute_statement(&query.statement, &store, &query, &mut warnings);
8424 assert!(result.is_err(), "should error when target grain not found");
8425 }
8426
8427 #[test]
8432 fn test_diff_grains_field_added() {
8433 let (_, grain_a) = make_fact("john", "likes", "coffee");
8434 let (_, mut grain_b) = make_fact("john", "likes", "coffee");
8435 grain_b
8436 .fields
8437 .insert("extra".into(), serde_json::json!("new_value"));
8438
8439 let diffs = super::diff_grains(&grain_a, &grain_b);
8440 let added = diffs.iter().find(|d| {
8441 matches!(d,
8442 super::super::ast::FieldDiff::Added { field, .. } if field == "extra"
8443 )
8444 });
8445 assert!(added.is_some(), "should detect added field 'extra'");
8446 }
8447
8448 #[test]
8449 fn test_diff_grains_field_removed() {
8450 let (_, mut grain_a) = make_fact("john", "likes", "coffee");
8451 let (_, grain_b) = make_fact("john", "likes", "coffee");
8452 grain_a
8453 .fields
8454 .insert("old_field".into(), serde_json::json!("old_value"));
8455
8456 let diffs = super::diff_grains(&grain_a, &grain_b);
8457 let removed = diffs.iter().find(|d| {
8458 matches!(d,
8459 super::super::ast::FieldDiff::Removed { field, .. } if field == "old_field"
8460 )
8461 });
8462 assert!(removed.is_some(), "should detect removed field 'old_field'");
8463 }
8464
8465 #[test]
8466 fn test_diff_grains_field_changed() {
8467 let (_, grain_a) = make_fact("john", "likes", "coffee");
8468 let (_, grain_b) = make_fact("john", "likes", "tea");
8469
8470 let diffs = super::diff_grains(&grain_a, &grain_b);
8471 let changed = diffs.iter().find(|d| {
8472 matches!(d,
8473 super::super::ast::FieldDiff::Changed { field, .. } if field == "object"
8474 )
8475 });
8476 assert!(changed.is_some(), "should detect changed field 'object'");
8477
8478 if let Some(super::super::ast::FieldDiff::Changed { old, new, .. }) = changed {
8479 assert_eq!(old.as_str().unwrap(), "coffee");
8480 assert_eq!(new.as_str().unwrap(), "tea");
8481 }
8482 }
8483
8484 #[test]
8485 fn test_diff_grains_identical() {
8486 let (_, grain_a) = make_fact("john", "likes", "vim");
8487 let (_, grain_b) = make_fact("john", "likes", "vim");
8488 let diffs = super::diff_grains(&grain_a, &grain_b);
8489 assert!(
8490 diffs.is_empty(),
8491 "identical grains should produce empty diff"
8492 );
8493 }
8494
8495 #[test]
8500 fn test_execute_describe_capabilities() {
8501 let store = MockStore::empty();
8502 let ex = exec();
8503 let result = ex.execute("DESCRIBE CAPABILITIES", &store).unwrap();
8504 assert_eq!(result.metadata.statement_type, "describe");
8505 match result.result {
8506 CalResultPayload::Describe { info } => {
8507 assert_eq!(info["cal_version"], 1);
8508 assert_eq!(info["conformance_level"], 2);
8509 assert!(!info["supported_statements"].as_array().unwrap().is_empty());
8510 assert_eq!(info["max_sources"], 8);
8511 assert_eq!(info["max_let_bindings"], 5);
8512 }
8513 other => panic!("expected Describe, got {:?}", other),
8514 }
8515 }
8516
8517 #[test]
8518 fn test_execute_describe_server() {
8519 let store = MockStore::empty();
8520 let ex = exec();
8521 let result = ex.execute("DESCRIBE SERVER", &store).unwrap();
8522 match result.result {
8523 CalResultPayload::Describe { info } => {
8524 assert_eq!(info["name"], "areev");
8525 assert!(info.get("version").is_some());
8526 assert_eq!(info["oms_version"], "1.2");
8527 assert!(info.get("build_features").is_some());
8528 }
8529 other => panic!("expected Describe, got {:?}", other),
8530 }
8531 }
8532
8533 #[test]
8534 fn test_execute_describe_fields() {
8535 let store = MockStore::empty();
8536 let ex = exec();
8537 let result = ex.execute("DESCRIBE FIELDS", &store).unwrap();
8538 match result.result {
8539 CalResultPayload::Describe { info } => {
8540 assert!(info.get("fields").is_some());
8541 let fields = info["fields"].as_array().unwrap();
8542 assert!(!fields.is_empty());
8543 let has_subject = fields.iter().any(|f| f["name"] == "subject");
8544 assert!(has_subject, "fields should contain 'subject'");
8545 }
8546 other => panic!("expected Describe, got {:?}", other),
8547 }
8548 }
8549
8550 #[test]
8551 fn test_execute_describe_templates() {
8552 let store = MockStore::empty();
8553 let ex = exec();
8554 let result = ex.execute("DESCRIBE TEMPLATES", &store).unwrap();
8555 match result.result {
8556 CalResultPayload::Describe { info } => {
8557 assert!(info.get("templates").is_some());
8558 let templates = info["templates"].as_array().unwrap();
8559 assert!(!templates.is_empty(), "templates list should not be empty");
8560 for name in ["structured", "readable", "compact"] {
8563 assert!(
8564 templates.iter().any(|t| t["name"] == name),
8565 "templates should include '{name}'"
8566 );
8567 }
8568 assert!(
8569 !templates.iter().any(|t| t["name"] == "toon"),
8570 "no builtin template may shadow the FORMAT toon arm"
8571 );
8572 }
8573 other => panic!("expected Describe, got {:?}", other),
8574 }
8575 }
8576
8577 #[test]
8578 fn test_execute_describe_grammar() {
8579 let store = MockStore::empty();
8580 let ex = exec();
8581 let result = ex.execute("DESCRIBE GRAMMAR", &store).unwrap();
8582 match result.result {
8583 CalResultPayload::Describe { info } => {
8584 assert_eq!(info["version"], 1);
8585 assert!(info.get("features").is_some());
8586 let features = info["features"].as_array().unwrap();
8587 assert!(!features.is_empty());
8588 assert_eq!(info["conformance_level"], 2);
8589 }
8590 other => panic!("expected Describe, got {:?}", other),
8591 }
8592 }
8593
8594 #[test]
8599 fn test_execute_explain_assemble() {
8600 let store = MockStore::empty();
8601 let ex = exec();
8602 let result = ex
8603 .execute(r#"EXPLAIN ASSEMBLE "summary" FROM (RECALL facts)"#, &store)
8604 .unwrap();
8605 match result.result {
8606 CalResultPayload::Explain { plan } => {
8607 assert_eq!(plan.statement_type, "assemble");
8608 assert!(plan.query_routing.contains("assemble"));
8609 assert!(plan.filters.iter().any(|f| f.contains("sources")));
8610 }
8611 other => panic!("expected Explain, got {:?}", other),
8612 }
8613 }
8614
8615 #[test]
8616 fn test_execute_explain_batch() {
8617 let store = MockStore::empty();
8618 let ex = exec();
8619 let result = ex
8620 .execute("EXPLAIN BATCH { RECALL facts ; RECALL events }", &store)
8621 .unwrap();
8622 match result.result {
8623 CalResultPayload::Explain { plan } => {
8624 assert_eq!(plan.statement_type, "batch");
8625 assert_eq!(plan.query_routing, "parallel_batch");
8626 assert!(plan
8627 .filters
8628 .iter()
8629 .any(|f| f.contains("parallel_execution")));
8630 }
8631 other => panic!("expected Explain, got {:?}", other),
8632 }
8633 }
8634
8635 #[test]
8636 fn test_execute_explain_coalesce() {
8637 let store = MockStore::empty();
8638 let ex = exec();
8639 let result = ex.execute(
8640 r#"EXPLAIN COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
8641 &store,
8642 ).unwrap();
8643 match result.result {
8644 CalResultPayload::Explain { plan } => {
8645 assert_eq!(plan.statement_type, "coalesce");
8646 assert_eq!(plan.query_routing, "coalesce_fallback");
8647 assert!(plan.filters.iter().any(|f| f.contains("fallback_chain")));
8648 }
8649 other => panic!("expected Explain, got {:?}", other),
8650 }
8651 }
8652
8653 #[test]
8654 fn test_execute_explain_describe_rejected() {
8655 let store = MockStore::empty();
8658 let ex = exec();
8659 let err = ex
8660 .execute("EXPLAIN DESCRIBE SCHEMA", &store)
8661 .expect_err("EXPLAIN DESCRIBE should be a parse error per §8.5");
8662 assert_eq!(err.code(), "CAL-E002");
8663 assert!(
8664 err.to_string().contains("DESCRIBE"),
8665 "error should mention DESCRIBE: {}",
8666 err
8667 );
8668 }
8669
8670 #[test]
8671 fn test_execute_explain_history() {
8672 let (hash, grain) = make_fact("john", "likes", "coffee");
8673 let store = MockStore::with_grains(vec![(hash, grain)]);
8674 let ex = exec();
8675 let hex = hash.to_hex();
8676 let query_str = format!("EXPLAIN HISTORY OF sha256:{}", hex);
8677 let result = ex.execute(&query_str, &store).unwrap();
8678 match result.result {
8679 CalResultPayload::Explain { plan } => {
8680 assert_eq!(plan.statement_type, "history");
8681 assert_eq!(plan.query_routing, "entity_latest");
8682 assert!(plan.index_usage.contains(&"entity_latest".to_string()));
8683 }
8684 other => panic!("expected Explain, got {:?}", other),
8685 }
8686 }
8687
8688 #[test]
8693 fn test_field_diff_serialization() {
8694 let diff = super::super::ast::FieldDiff::Changed {
8695 field: "object".into(),
8696 old: serde_json::json!("coffee"),
8697 new: serde_json::json!("tea"),
8698 };
8699 let json = serde_json::to_value(&diff).unwrap();
8700 assert_eq!(json["kind"], "changed");
8701 assert_eq!(json["field"], "object");
8702 assert_eq!(json["old"], "coffee");
8703 assert_eq!(json["new"], "tea");
8704 }
8705
8706 #[test]
8707 fn test_diff_payload_serialization() {
8708 let payload = CalResultPayload::Diff {
8709 source_hash: "aaa".into(),
8710 target_hash: "bbb".into(),
8711 changes: vec![
8712 super::super::ast::FieldDiff::Added {
8713 field: "new_field".into(),
8714 value: serde_json::json!("value"),
8715 },
8716 super::super::ast::FieldDiff::Removed {
8717 field: "old_field".into(),
8718 value: serde_json::json!(42),
8719 },
8720 ],
8721 };
8722 let json = serde_json::to_value(&payload).unwrap();
8723 assert_eq!(json["type"], "diff");
8724 assert_eq!(json["source_hash"], "aaa");
8725 assert_eq!(json["target_hash"], "bbb");
8726 assert_eq!(json["changes"].as_array().unwrap().len(), 2);
8727 }
8728
8729 #[test]
8730 fn test_build_features_list() {
8731 let features = super::build_features_list();
8732 assert!(features.contains(&"cal"), "cal feature should be active");
8733 }
8734
8735 #[test]
8736 fn test_count_payload_results_diff() {
8737 let payload = CalResultPayload::Diff {
8738 source_hash: "a".into(),
8739 target_hash: "b".into(),
8740 changes: vec![super::super::ast::FieldDiff::Added {
8741 field: "x".into(),
8742 value: serde_json::json!(1),
8743 }],
8744 };
8745 assert_eq!(super::count_payload_results(&payload), 1);
8746 }
8747
8748 #[test]
8755 fn test_i5_value_to_string_parameter_returns_error() {
8756 let val = Value::Parameter {
8757 name: "test".into(),
8758 };
8759 let result = super::value_to_string(&val);
8760 assert!(
8761 result.is_err(),
8762 "Parameter must return error, not \"$test\""
8763 );
8764 match result.unwrap_err() {
8765 CalError::UnboundParameter { name, .. } => {
8766 assert_eq!(name, "test");
8767 }
8768 other => panic!("expected UnboundParameter, got: {:?}", other),
8769 }
8770 }
8771
8772 #[test]
8775 fn test_i7_subjects_extractor_returns_grains() {
8776 let (hash, grain) = make_fact("john", "likes", "coffee");
8777 let store = MockStore::with_grains(vec![(hash, grain)]);
8778 let ex = exec();
8779 let result = ex.execute("RECALL facts SUBJECTS", &store).unwrap();
8780 match result.result {
8781 CalResultPayload::Grains { grains, .. } => {
8782 assert_eq!(grains.len(), 1);
8783 assert_eq!(grains[0].grain_type, "extracted");
8784 let value = grains[0].fields.get("value").unwrap();
8785 assert_eq!(value.as_str().unwrap(), "john");
8786 }
8787 other => panic!("SUBJECTS should return Grains (I-7 fix), got: {:?}", other),
8788 }
8789 }
8790
8791 #[test]
8792 fn test_i7_objects_extractor_returns_grains() {
8793 let (hash, grain) = make_fact("john", "likes", "coffee");
8794 let store = MockStore::with_grains(vec![(hash, grain)]);
8795 let ex = exec();
8796 let result = ex.execute("RECALL facts OBJECTS", &store).unwrap();
8797 match result.result {
8798 CalResultPayload::Grains { grains, .. } => {
8799 assert_eq!(grains.len(), 1);
8800 assert_eq!(grains[0].grain_type, "extracted");
8801 let value = grains[0].fields.get("value").unwrap();
8802 assert_eq!(value.as_str().unwrap(), "coffee");
8803 }
8804 other => panic!("OBJECTS should return Grains (I-7 fix), got: {:?}", other),
8805 }
8806 }
8807
8808 #[test]
8809 fn test_i7_hashes_extractor_returns_grains() {
8810 let (hash, grain) = make_fact("john", "likes", "coffee");
8811 let store = MockStore::with_grains(vec![(hash, grain)]);
8812 let ex = exec();
8813 let result = ex.execute("RECALL facts HASHES", &store).unwrap();
8814 match result.result {
8815 CalResultPayload::Grains { grains, .. } => {
8816 assert_eq!(grains.len(), 1);
8817 assert_eq!(grains[0].grain_type, "extracted");
8818 let value = grains[0].fields.get("value").unwrap();
8819 assert_eq!(value.as_str().unwrap(), hash.to_hex());
8820 }
8821 other => panic!("HASHES should return Grains (I-7 fix), got: {:?}", other),
8822 }
8823 }
8824
8825 #[test]
8828 fn test_let_binding_subjects_extractor() {
8829 let grains = vec![
8830 make_fact("john", "likes", "coffee"),
8831 make_fact("bob", "likes", "tea"),
8832 ];
8833 let store = MockStore::with_grains(grains);
8834 let ex = exec();
8835 let result = ex
8837 .execute(
8838 r#"LET $users = RECALL facts SUBJECTS; RECALL facts WHERE subject = "john""#,
8839 &store,
8840 )
8841 .unwrap();
8842 assert_eq!(result.metadata.statement_type, "recall");
8845 }
8846
8847 #[test]
8848 fn test_let_scope_evaluate_basic() {
8849 let grains = vec![
8850 make_fact("john", "likes", "coffee"),
8851 make_fact("bob", "likes", "coffee"),
8852 ];
8853 let store = MockStore::with_grains(grains);
8854 let ex = exec();
8855
8856 let binding = super::super::ast::LetBinding {
8857 name: "users".into(),
8858 extractor: super::super::ast::Extractor::Subjects,
8859 source: Box::new(CalStatement::Recall(RecallStmt {
8860 grain_type: GrainTypePlural::Facts,
8861 about: None,
8862 where_clause: None,
8863 recent: None,
8864 since: None,
8865 until: None,
8866 like: None,
8867 between: None,
8868 contradictions: None,
8869 limit: None,
8870 as_format: None,
8871 span: None,
8872 })),
8873 span: None,
8874 };
8875
8876 let query = CalQuery {
8877 version: super::super::ast::CalVersion(1),
8878 statement: CalStatement::Recall(RecallStmt {
8879 grain_type: GrainTypePlural::Facts,
8880 about: None,
8881 where_clause: None,
8882 recent: None,
8883 since: None,
8884 until: None,
8885 like: None,
8886 between: None,
8887 contradictions: None,
8888 limit: None,
8889 as_format: None,
8890 span: None,
8891 }),
8892 pipeline: Vec::new(),
8893 with_options: Vec::new(),
8894 format: None,
8895 let_bindings: vec![binding.clone()],
8896 let_values: Default::default(),
8897 user_vars: HashMap::new(),
8898 warnings: Vec::new(),
8899 };
8900
8901 let mut warnings = Vec::new();
8902 let scope =
8903 super::LetScope::evaluate(&[binding], &ex, &store, &query, &mut warnings).unwrap();
8904
8905 match scope.resolve("users").unwrap() {
8906 super::LetValue::Extracted(values) => {
8907 assert!(values.contains(&"john".to_string()));
8908 assert!(values.contains(&"bob".to_string()));
8909 }
8910 other => panic!("expected Extracted, got: {:?}", other),
8911 }
8912
8913 assert!(scope.resolve("missing").is_err());
8915 }
8916
8917 #[test]
8918 fn test_let_scope_too_many_bindings_s06() {
8919 let store = MockStore::empty();
8920 let ex = exec();
8921
8922 let bindings: Vec<super::super::ast::LetBinding> = (0..6)
8924 .map(|i| super::super::ast::LetBinding {
8925 name: format!("var{}", i),
8926 extractor: super::super::ast::Extractor::Subjects,
8927 source: Box::new(CalStatement::Recall(RecallStmt {
8928 grain_type: GrainTypePlural::Facts,
8929 about: None,
8930 where_clause: None,
8931 recent: None,
8932 since: None,
8933 until: None,
8934 like: None,
8935 between: None,
8936 contradictions: None,
8937 limit: None,
8938 as_format: None,
8939 span: None,
8940 })),
8941 span: None,
8942 })
8943 .collect();
8944
8945 let query = CalQuery {
8946 version: super::super::ast::CalVersion(1),
8947 statement: CalStatement::Recall(RecallStmt {
8948 grain_type: GrainTypePlural::Facts,
8949 about: None,
8950 where_clause: None,
8951 recent: None,
8952 since: None,
8953 until: None,
8954 like: None,
8955 between: None,
8956 contradictions: None,
8957 limit: None,
8958 as_format: None,
8959 span: None,
8960 }),
8961 pipeline: Vec::new(),
8962 with_options: Vec::new(),
8963 format: None,
8964 let_bindings: bindings.clone(),
8965 let_values: Default::default(),
8966 user_vars: HashMap::new(),
8967 warnings: Vec::new(),
8968 };
8969
8970 let mut warnings = Vec::new();
8971 let result = super::LetScope::evaluate(&bindings, &ex, &store, &query, &mut warnings);
8972 assert!(result.is_err(), "6 bindings must exceed S-06 limit of 5");
8973 match result.unwrap_err() {
8974 CalError::TooManyLetBindings { count, max, .. } => {
8975 assert_eq!(count, 6);
8976 assert_eq!(max, 5);
8977 }
8978 other => panic!("expected TooManyLetBindings, got: {:?}", other),
8979 }
8980 }
8981
8982 #[test]
8985 fn test_redact_budget_metadata_config_default() {
8986 let config = CalExecutorConfig::default();
8987 assert!(
8988 !config.redact_budget_metadata,
8989 "default should be false (S-09)"
8990 );
8991 }
8992
8993 #[test]
8994 fn test_redact_budget_metadata_config_override() {
8995 let config = CalExecutorConfig {
8996 redact_budget_metadata: true,
8997 ..Default::default()
8998 };
8999 assert!(config.redact_budget_metadata);
9000 }
9001
9002 #[test]
9005 fn test_count_payload_results_assembled() {
9006 let payload = CalResultPayload::Assembled {
9007 grains: vec![CalGrainResult {
9008 hash: "abc".into(),
9009 grain_type: "fact".into(),
9010 score: 1.0,
9011 fields: serde_json::json!({}),
9012 score_breakdown: None,
9013 explanation: None,
9014 relative_time: None,
9015 is_deterministic: false,
9016 contested_by: None,
9017 }],
9018 sources: vec![],
9019 total_tokens: 100,
9020 budget_limit: Some(500),
9021 progressive: false,
9022 total_available: Some(1),
9023 };
9024 assert_eq!(super::count_payload_results(&payload), 1);
9025 }
9026
9027 #[test]
9034 fn test_labeled_batch_returns_keyed_results() {
9035 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
9036 let ex = exec();
9037
9038 let batch = super::super::ast::BatchStmt {
9040 statements: Vec::new(),
9041 labeled: Some(vec![
9042 (
9043 "prefs".to_string(),
9044 super::super::ast::BatchEntry {
9045 statement: CalStatement::Recall(RecallStmt {
9046 grain_type: GrainTypePlural::Facts,
9047 about: None,
9048 where_clause: Some(super::super::ast::WhereClause {
9049 condition: super::super::ast::Condition::Comparison {
9050 field: "subject".into(),
9051 comparator: super::super::ast::Comparator::Eq,
9052 value: super::super::ast::Value::String {
9053 value: "john".into(),
9054 },
9055 span: None,
9056 },
9057 span: None,
9058 }),
9059 recent: None,
9060 since: None,
9061 until: None,
9062 like: None,
9063 between: None,
9064 contradictions: None,
9065 limit: None,
9066 as_format: None,
9067 span: None,
9068 }),
9069 pipeline: Vec::new(),
9070 with_options: Vec::new(),
9071 format: None,
9072 user_vars: HashMap::new(),
9073 },
9074 ),
9075 (
9076 "all".to_string(),
9077 super::super::ast::BatchEntry {
9078 statement: CalStatement::Recall(RecallStmt {
9079 grain_type: GrainTypePlural::Facts,
9080 about: None,
9081 where_clause: None,
9082 recent: None,
9083 since: None,
9084 until: None,
9085 like: None,
9086 between: None,
9087 contradictions: None,
9088 limit: None,
9089 as_format: None,
9090 span: None,
9091 }),
9092 pipeline: Vec::new(),
9093 with_options: Vec::new(),
9094 format: None,
9095 user_vars: HashMap::new(),
9096 },
9097 ),
9098 ]),
9099 span: None,
9100 };
9101
9102 let mut warnings = Vec::new();
9103 let result = ex.execute_batch(&batch, &store, &mut warnings).unwrap();
9104
9105 match result {
9106 CalResultPayload::Batch { results } => {
9107 assert!(results.contains_key("prefs"), "should have 'prefs' label");
9108 assert!(results.contains_key("all"), "should have 'all' label");
9109 assert_eq!(results.len(), 2, "exactly 2 labeled results");
9110 }
9111 other => panic!("expected Batch, got: {:?}", other),
9112 }
9113 }
9114
9115 #[test]
9116 fn test_labeled_batch_duplicate_label_errors() {
9117 let store = MockStore::empty();
9118 let ex = exec();
9119
9120 let batch = super::super::ast::BatchStmt {
9121 statements: Vec::new(),
9122 labeled: Some(vec![
9123 (
9124 "dup".to_string(),
9125 super::super::ast::BatchEntry {
9126 statement: CalStatement::Recall(RecallStmt {
9127 grain_type: GrainTypePlural::Facts,
9128 about: None,
9129 where_clause: None,
9130 recent: None,
9131 since: None,
9132 until: None,
9133 like: None,
9134 between: None,
9135 contradictions: None,
9136 limit: None,
9137 as_format: None,
9138 span: None,
9139 }),
9140 pipeline: Vec::new(),
9141 with_options: Vec::new(),
9142 format: None,
9143 user_vars: HashMap::new(),
9144 },
9145 ),
9146 (
9147 "dup".to_string(),
9148 super::super::ast::BatchEntry {
9149 statement: CalStatement::Recall(RecallStmt {
9150 grain_type: GrainTypePlural::Events,
9151 about: None,
9152 where_clause: None,
9153 recent: None,
9154 since: None,
9155 until: None,
9156 like: None,
9157 between: None,
9158 contradictions: None,
9159 limit: None,
9160 as_format: None,
9161 span: None,
9162 }),
9163 pipeline: Vec::new(),
9164 with_options: Vec::new(),
9165 format: None,
9166 user_vars: HashMap::new(),
9167 },
9168 ),
9169 ]),
9170 span: None,
9171 };
9172
9173 let mut warnings = Vec::new();
9174 let result = ex.execute_batch(&batch, &store, &mut warnings);
9175 assert!(result.is_err());
9176 match result.unwrap_err() {
9177 CalError::AssembleDuplicateLabel { label, .. } => {
9178 assert_eq!(label, "dup");
9179 }
9180 other => panic!("expected AssembleDuplicateLabel, got: {:?}", other),
9181 }
9182 }
9183
9184 #[test]
9185 fn test_positional_batch_still_works() {
9186 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
9188 let ex = exec();
9189
9190 let batch = super::super::ast::BatchStmt {
9191 statements: vec![super::super::ast::BatchEntry {
9192 statement: CalStatement::Recall(RecallStmt {
9193 grain_type: GrainTypePlural::Facts,
9194 about: None,
9195 where_clause: None,
9196 recent: None,
9197 since: None,
9198 until: None,
9199 like: None,
9200 between: None,
9201 contradictions: None,
9202 limit: None,
9203 as_format: None,
9204 span: None,
9205 }),
9206 pipeline: Vec::new(),
9207 with_options: Vec::new(),
9208 format: None,
9209 user_vars: HashMap::new(),
9210 }],
9211 labeled: None,
9212 span: None,
9213 };
9214
9215 let mut warnings = Vec::new();
9216 let result = ex.execute_batch(&batch, &store, &mut warnings).unwrap();
9217 match result {
9218 CalResultPayload::Batch { results } => {
9219 assert!(
9220 results.contains_key("0"),
9221 "positional batch uses index keys"
9222 );
9223 }
9224 other => panic!("expected Batch, got: {:?}", other),
9225 }
9226 }
9227
9228 #[test]
9231 fn test_coalesce_multibranch_first_hit_wins() {
9232 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
9233 let ex = exec();
9234
9235 let coalesce = super::super::ast::CoalesceStmt {
9236 grain_type: GrainTypePlural::Facts,
9237 where_clause: None,
9238 branches: vec![
9239 super::super::ast::CoalesceBranch {
9241 query: CalStatement::Recall(RecallStmt {
9242 grain_type: GrainTypePlural::Facts,
9243 about: None,
9244 where_clause: Some(super::super::ast::WhereClause {
9245 condition: super::super::ast::Condition::Comparison {
9246 field: "subject".into(),
9247 comparator: super::super::ast::Comparator::Eq,
9248 value: super::super::ast::Value::String {
9249 value: "john".into(),
9250 },
9251 span: None,
9252 },
9253 span: None,
9254 }),
9255 recent: None,
9256 since: None,
9257 until: None,
9258 like: None,
9259 between: None,
9260 contradictions: None,
9261 limit: None,
9262 as_format: None,
9263 span: None,
9264 }),
9265 span: None,
9266 },
9267 super::super::ast::CoalesceBranch {
9269 query: CalStatement::Recall(RecallStmt {
9270 grain_type: GrainTypePlural::Events,
9271 about: None,
9272 where_clause: None,
9273 recent: None,
9274 since: None,
9275 until: None,
9276 like: None,
9277 between: None,
9278 contradictions: None,
9279 limit: None,
9280 as_format: None,
9281 span: None,
9282 }),
9283 span: None,
9284 },
9285 ],
9286 else_branch: None,
9287 span: None,
9288 };
9289
9290 let query = CalQuery {
9291 version: super::super::ast::CalVersion(1),
9292 statement: CalStatement::Coalesce(coalesce.clone()),
9293 pipeline: Vec::new(),
9294 with_options: Vec::new(),
9295 format: None,
9296 let_bindings: Vec::new(),
9297 let_values: Default::default(),
9298 user_vars: HashMap::new(),
9299 warnings: Vec::new(),
9300 };
9301
9302 let mut warnings = Vec::new();
9303 let result = ex
9304 .execute_coalesce(&coalesce, &store, &query, &mut warnings)
9305 .unwrap();
9306
9307 match result {
9308 CalResultPayload::Grains { grains, .. } => {
9309 assert_eq!(grains.len(), 1, "branch 1 should return john");
9310 }
9311 other => panic!("expected Grains, got: {:?}", other),
9312 }
9313
9314 assert!(
9316 warnings.iter().any(|w| w.contains("short-circuited")),
9317 "should emit short-circuit warning"
9318 );
9319 }
9320
9321 #[test]
9322 fn test_coalesce_multibranch_falls_through_to_else() {
9323 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
9324 let ex = exec();
9325
9326 let coalesce = super::super::ast::CoalesceStmt {
9327 grain_type: GrainTypePlural::Facts,
9328 where_clause: None,
9329 branches: vec![
9330 super::super::ast::CoalesceBranch {
9332 query: CalStatement::Recall(RecallStmt {
9333 grain_type: GrainTypePlural::Facts,
9334 about: None,
9335 where_clause: Some(super::super::ast::WhereClause {
9336 condition: super::super::ast::Condition::Comparison {
9337 field: "subject".into(),
9338 comparator: super::super::ast::Comparator::Eq,
9339 value: super::super::ast::Value::String {
9340 value: "zzz".into(),
9341 },
9342 span: None,
9343 },
9344 span: None,
9345 }),
9346 recent: None,
9347 since: None,
9348 until: None,
9349 like: None,
9350 between: None,
9351 contradictions: None,
9352 limit: None,
9353 as_format: None,
9354 span: None,
9355 }),
9356 span: None,
9357 },
9358 ],
9359 else_branch: Some(Box::new(CalStatement::Recall(RecallStmt {
9361 grain_type: GrainTypePlural::Facts,
9362 about: None,
9363 where_clause: None,
9364 recent: None,
9365 since: None,
9366 until: None,
9367 like: None,
9368 between: None,
9369 contradictions: None,
9370 limit: None,
9371 as_format: None,
9372 span: None,
9373 }))),
9374 span: None,
9375 };
9376
9377 let query = CalQuery {
9378 version: super::super::ast::CalVersion(1),
9379 statement: CalStatement::Coalesce(coalesce.clone()),
9380 pipeline: Vec::new(),
9381 with_options: Vec::new(),
9382 format: None,
9383 let_bindings: Vec::new(),
9384 let_values: Default::default(),
9385 user_vars: HashMap::new(),
9386 warnings: Vec::new(),
9387 };
9388
9389 let mut warnings = Vec::new();
9390 let result = ex
9391 .execute_coalesce(&coalesce, &store, &query, &mut warnings)
9392 .unwrap();
9393
9394 match result {
9395 CalResultPayload::Grains { grains, .. } => {
9396 assert_eq!(grains.len(), 1, "ELSE branch should return john");
9397 }
9398 other => panic!("expected Grains, got: {:?}", other),
9399 }
9400 }
9401
9402 #[test]
9403 fn test_coalesce_multibranch_all_empty_no_else() {
9404 let store = MockStore::empty();
9405 let ex = exec();
9406
9407 let coalesce = super::super::ast::CoalesceStmt {
9408 grain_type: GrainTypePlural::Facts,
9409 where_clause: None,
9410 branches: vec![super::super::ast::CoalesceBranch {
9411 query: CalStatement::Recall(RecallStmt {
9412 grain_type: GrainTypePlural::Facts,
9413 about: None,
9414 where_clause: None,
9415 recent: None,
9416 since: None,
9417 until: None,
9418 like: None,
9419 between: None,
9420 contradictions: None,
9421 limit: None,
9422 as_format: None,
9423 span: None,
9424 }),
9425 span: None,
9426 }],
9427 else_branch: None,
9428 span: None,
9429 };
9430
9431 let query = CalQuery {
9432 version: super::super::ast::CalVersion(1),
9433 statement: CalStatement::Coalesce(coalesce.clone()),
9434 pipeline: Vec::new(),
9435 with_options: Vec::new(),
9436 format: None,
9437 let_bindings: Vec::new(),
9438 let_values: Default::default(),
9439 user_vars: HashMap::new(),
9440 warnings: Vec::new(),
9441 };
9442
9443 let mut warnings = Vec::new();
9444 let result = ex
9445 .execute_coalesce(&coalesce, &store, &query, &mut warnings)
9446 .unwrap();
9447
9448 match result {
9449 CalResultPayload::Grains { grains, .. } => {
9450 assert!(grains.is_empty(), "all empty, no ELSE → empty result");
9451 }
9452 other => panic!("expected Grains, got: {:?}", other),
9453 }
9454 }
9455
9456 #[test]
9459 fn test_explain_reports_tier1_disabled() {
9460 let store = MockStore::empty();
9461 let ex = CalExecutor::new(CalExecutorConfig {
9462 tier1_enabled: false,
9463 ..Default::default()
9464 });
9465 let result = ex.execute("EXPLAIN RECALL facts", &store).unwrap();
9466
9467 match result.result {
9468 CalResultPayload::Explain { plan } => {
9469 assert!(
9470 plan.filters.iter().any(|f| f.contains("tier1_disabled")),
9471 "should report tier1_disabled: {:?}",
9472 plan.filters
9473 );
9474 }
9475 other => panic!("expected Explain, got: {:?}", other),
9476 }
9477 }
9478
9479 #[test]
9480 fn test_explain_reports_namespace_override() {
9481 let store = MockStore::empty();
9482 let ex = CalExecutor::new(CalExecutorConfig {
9483 namespace_override: Some("test_ns".to_string()),
9484 ..Default::default()
9485 });
9486 let result = ex.execute("EXPLAIN RECALL facts", &store).unwrap();
9487
9488 match result.result {
9489 CalResultPayload::Explain { plan } => {
9490 assert!(
9491 plan.filters
9492 .iter()
9493 .any(|f| f.contains("namespace_override")),
9494 "should report namespace_override: {:?}",
9495 plan.filters
9496 );
9497 }
9498 other => panic!("expected Explain, got: {:?}", other),
9499 }
9500 }
9501
9502 #[test]
9503 fn test_explain_reports_user_id_override() {
9504 let store = MockStore::empty();
9505 let ex = CalExecutor::new(CalExecutorConfig {
9506 user_id_override: Some("user123".to_string()),
9507 ..Default::default()
9508 });
9509 let result = ex.execute("EXPLAIN RECALL facts", &store).unwrap();
9510
9511 match result.result {
9512 CalResultPayload::Explain { plan } => {
9513 assert!(
9514 plan.filters.iter().any(|f| f.contains("user_id_override")),
9515 "should report user_id_override: {:?}",
9516 plan.filters
9517 );
9518 }
9519 other => panic!("expected Explain, got: {:?}", other),
9520 }
9521 }
9522
9523 #[test]
9526 fn test_type_specific_fields_tools() {
9527 let fields = super::type_specific_fields(&GrainTypePlural::Tools);
9528 assert!(fields.contains(&"tool"), "Tools should have 'tool' field");
9529 assert!(
9530 fields.contains(&"is_error"),
9531 "Tools should have 'is_error' field"
9532 );
9533 assert!(
9534 fields.contains(&"duration_ms"),
9535 "Tools should have 'duration_ms' field"
9536 );
9537 }
9538
9539 #[test]
9540 fn test_type_specific_fields_goals() {
9541 let fields = super::type_specific_fields(&GrainTypePlural::Goals);
9542 assert!(fields.contains(&"title"), "Goals should have 'title' field");
9543 assert!(super::COMMON_FIELDS.contains(&"priority"));
9547 assert!(super::COMMON_FIELDS.contains(&"status"));
9548 }
9549
9550 #[test]
9551 fn test_type_specific_fields_all_returns_empty() {
9552 let fields = super::type_specific_fields(&GrainTypePlural::All);
9553 assert!(
9554 fields.is_empty(),
9555 "All should return empty (no type-specific validation)"
9556 );
9557 }
9558
9559 #[test]
9560 fn test_is_known_type_specific_field() {
9561 assert!(
9562 super::is_known_type_specific_field("tool"),
9563 "'tool' is known (on Tools)"
9564 );
9565 assert!(super::COMMON_FIELDS.contains(&"priority"));
9568 assert!(
9569 !super::is_known_type_specific_field("zzz_unknown"),
9570 "'zzz_unknown' is not known"
9571 );
9572 }
9573
9574 #[test]
9575 fn test_grain_matches_condition_string_eq() {
9576 let grain = CalGrainResult {
9577 hash: "abc".into(),
9578 grain_type: "tool".into(),
9579 score: 1.0,
9580 fields: serde_json::json!({
9581 "tool": "web_search",
9582 "is_error": false,
9583 "duration_ms": 150
9584 }),
9585 score_breakdown: None,
9586 explanation: None,
9587 relative_time: None,
9588 is_deterministic: false,
9589 contested_by: None,
9590 };
9591
9592 assert!(super::grain_matches_condition(
9594 &grain,
9595 "tool",
9596 &super::super::ast::Comparator::Eq,
9597 &super::super::ast::Value::String {
9598 value: "web_search".into()
9599 }
9600 ));
9601 assert!(!super::grain_matches_condition(
9602 &grain,
9603 "tool",
9604 &super::super::ast::Comparator::Eq,
9605 &super::super::ast::Value::String {
9606 value: "db_query".into()
9607 }
9608 ));
9609 }
9610
9611 #[test]
9612 fn test_grain_matches_condition_number_comparisons() {
9613 let grain = CalGrainResult {
9614 hash: "abc".into(),
9615 grain_type: "tool".into(),
9616 score: 1.0,
9617 fields: serde_json::json!({
9618 "duration_ms": 150.0
9619 }),
9620 score_breakdown: None,
9621 explanation: None,
9622 relative_time: None,
9623 is_deterministic: false,
9624 contested_by: None,
9625 };
9626
9627 assert!(super::grain_matches_condition(
9629 &grain,
9630 "duration_ms",
9631 &super::super::ast::Comparator::Gte,
9632 &super::super::ast::Value::Number { value: 100.0 }
9633 ));
9634 assert!(super::grain_matches_condition(
9635 &grain,
9636 "duration_ms",
9637 &super::super::ast::Comparator::Gte,
9638 &super::super::ast::Value::Number { value: 150.0 }
9639 ));
9640 assert!(!super::grain_matches_condition(
9641 &grain,
9642 "duration_ms",
9643 &super::super::ast::Comparator::Gte,
9644 &super::super::ast::Value::Number { value: 200.0 }
9645 ));
9646
9647 assert!(super::grain_matches_condition(
9649 &grain,
9650 "duration_ms",
9651 &super::super::ast::Comparator::Lt,
9652 &super::super::ast::Value::Number { value: 200.0 }
9653 ));
9654 assert!(!super::grain_matches_condition(
9655 &grain,
9656 "duration_ms",
9657 &super::super::ast::Comparator::Lt,
9658 &super::super::ast::Value::Number { value: 150.0 }
9659 ));
9660 }
9661
9662 #[test]
9663 fn test_grain_matches_condition_boolean() {
9664 let grain = CalGrainResult {
9665 hash: "abc".into(),
9666 grain_type: "tool".into(),
9667 score: 1.0,
9668 fields: serde_json::json!({
9669 "is_error": false
9670 }),
9671 score_breakdown: None,
9672 explanation: None,
9673 relative_time: None,
9674 is_deterministic: false,
9675 contested_by: None,
9676 };
9677
9678 assert!(super::grain_matches_condition(
9679 &grain,
9680 "is_error",
9681 &super::super::ast::Comparator::Eq,
9682 &super::super::ast::Value::Boolean { value: false }
9683 ));
9684 assert!(!super::grain_matches_condition(
9685 &grain,
9686 "is_error",
9687 &super::super::ast::Comparator::Eq,
9688 &super::super::ast::Value::Boolean { value: true }
9689 ));
9690 }
9691
9692 #[test]
9693 fn test_grain_matches_condition_not_eq() {
9694 let grain = CalGrainResult {
9695 hash: "abc".into(),
9696 grain_type: "tool".into(),
9697 score: 1.0,
9698 fields: serde_json::json!({
9699 "tool": "web_search"
9700 }),
9701 score_breakdown: None,
9702 explanation: None,
9703 relative_time: None,
9704 is_deterministic: false,
9705 contested_by: None,
9706 };
9707
9708 assert!(super::grain_matches_condition(
9709 &grain,
9710 "tool",
9711 &super::super::ast::Comparator::NotEq,
9712 &super::super::ast::Value::String {
9713 value: "db_query".into()
9714 }
9715 ));
9716 assert!(!super::grain_matches_condition(
9717 &grain,
9718 "tool",
9719 &super::super::ast::Comparator::NotEq,
9720 &super::super::ast::Value::String {
9721 value: "web_search".into()
9722 }
9723 ));
9724 }
9725
9726 #[test]
9727 fn test_grain_matches_condition_missing_field_returns_false() {
9728 let grain = CalGrainResult {
9729 hash: "abc".into(),
9730 grain_type: "tool".into(),
9731 score: 1.0,
9732 fields: serde_json::json!({}),
9733 score_breakdown: None,
9734 explanation: None,
9735 relative_time: None,
9736 is_deterministic: false,
9737 contested_by: None,
9738 };
9739
9740 assert!(!super::grain_matches_condition(
9742 &grain,
9743 "tool",
9744 &super::super::ast::Comparator::Eq,
9745 &super::super::ast::Value::String {
9746 value: "anything".into()
9747 }
9748 ));
9749 assert!(super::grain_matches_condition(
9751 &grain,
9752 "tool",
9753 &super::super::ast::Comparator::NotEq,
9754 &super::super::ast::Value::String {
9755 value: "anything".into()
9756 }
9757 ));
9758 }
9759
9760 #[test]
9761 fn test_plan_residual_where_splits_pushdown_from_residual() {
9762 let condition = super::super::ast::Condition::And {
9765 left: Box::new(super::super::ast::Condition::Comparison {
9766 field: "subject".into(), comparator: super::super::ast::Comparator::Eq,
9768 value: super::super::ast::Value::String {
9769 value: "john".into(),
9770 },
9771 span: None,
9772 }),
9773 right: Box::new(super::super::ast::Condition::Comparison {
9774 field: "tool".into(), comparator: super::super::ast::Comparator::Eq,
9776 value: super::super::ast::Value::String {
9777 value: "web_search".into(),
9778 },
9779 span: None,
9780 }),
9781 span: None,
9782 };
9783
9784 let mut warnings = Vec::new();
9785 let residual =
9786 super::plan_residual_where(&condition, &GrainTypePlural::Tools, &mut warnings)
9787 .expect("plan must succeed")
9788 .expect("the type-specific leaf must remain residual");
9789 match residual {
9790 super::super::ast::Condition::Comparison { field, .. } => {
9791 assert_eq!(field, "tool");
9792 }
9793 other => panic!("expected the bare 'tool' leaf, got {other:?}"),
9794 }
9795 assert!(warnings.is_empty(), "no warnings for valid fields");
9796 }
9797
9798 #[test]
9799 fn test_plan_residual_where_unpushed_comparator_on_common_field() {
9800 let condition = super::super::ast::Condition::Comparison {
9804 field: "confidence".into(),
9805 comparator: super::super::ast::Comparator::Lt,
9806 value: super::super::ast::Value::Number { value: 0.5 },
9807 span: None,
9808 };
9809
9810 let mut warnings = Vec::new();
9811 let residual =
9812 super::plan_residual_where(&condition, &GrainTypePlural::Facts, &mut warnings)
9813 .expect("plan must succeed");
9814 assert!(
9815 residual.is_some(),
9816 "an unpushed comparator on a common field must be post-filtered"
9817 );
9818 }
9819
9820 #[test]
9821 fn test_plan_residual_where_refuses_unfilterable_field_on_typed_recall() {
9822 let condition = super::super::ast::Condition::Comparison {
9825 field: "priority".into(),
9826 comparator: super::super::ast::Comparator::Eq,
9827 value: super::super::ast::Value::String {
9828 value: "high".into(),
9829 },
9830 span: None,
9831 };
9832
9833 let mut warnings = Vec::new();
9834 let err = super::plan_residual_where(&condition, &GrainTypePlural::Tools, &mut warnings)
9835 .expect_err("a filter that cannot be honoured must refuse");
9836 assert_eq!(err.code(), "CAL-E060");
9837 }
9838
9839 #[test]
9840 fn test_plan_residual_where_refuses_engine_field_under_not() {
9841 let condition = super::super::ast::Condition::Not {
9844 inner: Box::new(super::super::ast::Condition::Comparison {
9845 field: "query".into(),
9846 comparator: super::super::ast::Comparator::Eq,
9847 value: super::super::ast::Value::String { value: "x".into() },
9848 span: None,
9849 }),
9850 span: None,
9851 };
9852
9853 let mut warnings = Vec::new();
9854 let err = super::plan_residual_where(&condition, &GrainTypePlural::Facts, &mut warnings)
9855 .expect_err("engine-only fields under NOT must refuse");
9856 assert_eq!(err.code(), "CAL-E061");
9857 }
9858
9859 #[test]
9860 fn test_plan_residual_where_keeps_not_subtree_whole() {
9861 let condition = super::super::ast::Condition::Not {
9864 inner: Box::new(super::super::ast::Condition::Comparison {
9865 field: "tool_name".into(),
9866 comparator: super::super::ast::Comparator::Eq,
9867 value: super::super::ast::Value::String { value: "x".into() },
9868 span: None,
9869 }),
9870 span: None,
9871 };
9872
9873 let mut warnings = Vec::new();
9874 let residual =
9875 super::plan_residual_where(&condition, &GrainTypePlural::Tools, &mut warnings)
9876 .expect("plan must succeed")
9877 .expect("NOT subtree must remain residual");
9878 assert!(
9879 matches!(residual, super::super::ast::Condition::Not { .. }),
9880 "the negation must be preserved"
9881 );
9882 }
9883
9884 #[test]
9885 fn test_pushdown_consumed_matches_apply_where_clause_arms() {
9886 use super::super::ast::{Comparator, Condition, Value};
9893 let cmp = |field: &str, comparator: Comparator| Condition::Comparison {
9894 field: field.into(),
9895 comparator,
9896 value: Value::String { value: "v".into() },
9897 span: None,
9898 };
9899
9900 for f in [
9902 "subject", "relation", "object", "namespace", "user_id", "query", "time", "entity",
9903 "scope", "scope_path",
9904 ] {
9905 assert!(super::leaf_pushdown_consumed(&cmp(f, Comparator::Eq)), "{f} Eq");
9906 assert!(!super::leaf_pushdown_consumed(&cmp(f, Comparator::NotEq)), "{f} NotEq");
9908 }
9909 for f in ["confidence", "importance"] {
9910 assert!(super::leaf_pushdown_consumed(&cmp(f, Comparator::Gte)), "{f} Gte");
9911 assert!(!super::leaf_pushdown_consumed(&cmp(f, Comparator::Lt)), "{f} Lt");
9912 }
9913 assert!(super::leaf_pushdown_consumed(&cmp("contradicted", Comparator::Eq)));
9914
9915 assert!(!super::leaf_pushdown_consumed(&cmp("hash", Comparator::Eq)));
9917 assert!(!super::leaf_pushdown_consumed(&cmp("session_id", Comparator::Eq)));
9918 assert!(!super::leaf_pushdown_consumed(&cmp("tool_name", Comparator::Eq)));
9919 assert!(!super::leaf_pushdown_consumed(&cmp("status", Comparator::Eq)));
9920
9921 let in_cond = |field: &str| Condition::In {
9923 field: field.into(),
9924 values: vec![Value::String { value: "v".into() }],
9925 span: None,
9926 };
9927 for f in ["subject", "relation", "object", "tags", "namespace"] {
9928 assert!(super::leaf_pushdown_consumed(&in_cond(f)), "{f} IN");
9929 }
9930 assert!(!super::leaf_pushdown_consumed(&in_cond("role")));
9931 let not_in_tags = Condition::NotIn {
9932 field: "tags".into(),
9933 values: vec![Value::String { value: "v".into() }],
9934 span: None,
9935 };
9936 assert!(super::leaf_pushdown_consumed(¬_in_tags));
9937 }
9938
9939 #[test]
9940 fn test_suggest_field_cross_type() {
9941 let suggestion = super::suggest_field("tool", &GrainTypePlural::Goals);
9943 assert!(
9944 suggestion.is_some(),
9945 "should suggest 'tool' exists on another type"
9946 );
9947 assert!(
9948 suggestion.unwrap().contains("different grain type"),
9949 "should mention different grain type"
9950 );
9951 }
9952
9953 #[test]
9954 fn test_suggest_field_substring_match() {
9955 let suggestion = super::suggest_field("stat", &GrainTypePlural::Goals);
9957 assert!(
9958 suggestion.is_some(),
9959 "should suggest 'status' for substring 'stat'"
9960 );
9961 }
9962
9963 #[test]
9966 fn test_type_specific_fields_events() {
9967 let fields = super::type_specific_fields(&GrainTypePlural::Events);
9968 assert!(fields.contains(&"session_id"));
9969 assert!(fields.contains(&"content"));
9970 assert!(fields.contains(&"created_at"));
9971 }
9972
9973 #[test]
9974 fn test_type_specific_fields_states() {
9975 let fields = super::type_specific_fields(&GrainTypePlural::States);
9976 assert!(fields.contains(&"context"));
9981 assert!(fields.contains(&"plan"));
9982 assert!(fields.contains(&"history"));
9983 assert!(!fields.contains(&"checkpoint_data"));
9984 assert!(!fields.contains(&"session_id"));
9985 }
9986
9987 #[test]
9988 fn test_type_specific_fields_consents() {
9989 let fields = super::type_specific_fields(&GrainTypePlural::Consents);
9990 assert!(super::COMMON_FIELDS.contains(&"scope"));
9993 assert!(fields.contains(&"granted"));
9994 assert!(fields.contains(&"subject_did"));
9995 assert!(fields.contains(&"grantee_did"));
9996 }
9997
9998 #[test]
9999 fn test_type_specific_fields_observations() {
10000 let fields = super::type_specific_fields(&GrainTypePlural::Observations);
10001 assert!(fields.contains(&"sensor"));
10002 assert!(fields.contains(&"value"));
10003 assert!(fields.contains(&"unit"));
10004 }
10005
10006 #[test]
10007 fn test_type_specific_fields_workflows() {
10008 let fields = super::type_specific_fields(&GrainTypePlural::Workflows);
10009 assert!(fields.contains(&"name"));
10010 assert!(super::COMMON_FIELDS.contains(&"status"));
10013 assert!(fields.contains(&"nodes"));
10014 }
10015
10016 #[test]
10017 fn test_type_specific_fields_reasonings() {
10018 let fields = super::type_specific_fields(&GrainTypePlural::Reasonings);
10019 assert!(fields.contains(&"premises"));
10020 assert!(fields.contains(&"conclusion"));
10021 assert!(super::COMMON_FIELDS.contains(&"confidence"));
10023 }
10024
10025 #[test]
10026 fn test_session_id_not_in_common_fields() {
10027 assert!(
10031 !super::COMMON_FIELDS.contains(&"session_id"),
10032 "session_id should not be in COMMON_FIELDS"
10033 );
10034 }
10035
10036 #[test]
10037 fn test_session_id_stays_residual_despite_scan_pushdown() {
10038 let condition = super::super::ast::Condition::And {
10039 left: Box::new(super::super::ast::Condition::Comparison {
10040 field: "subject".into(),
10041 comparator: super::super::ast::Comparator::Eq,
10042 value: super::super::ast::Value::String {
10043 value: "alice".into(),
10044 },
10045 span: None,
10046 }),
10047 right: Box::new(super::super::ast::Condition::Comparison {
10048 field: "session_id".into(),
10049 comparator: super::super::ast::Comparator::Eq,
10050 value: super::super::ast::Value::String {
10051 value: "sess-001".into(),
10052 },
10053 span: None,
10054 }),
10055 span: None,
10056 };
10057
10058 let mut warnings = Vec::new();
10059 let residual =
10060 super::plan_residual_where(&condition, &GrainTypePlural::Events, &mut warnings)
10061 .expect("plan must succeed")
10062 .expect("session_id must remain residual");
10063 match residual {
10067 super::super::ast::Condition::Comparison { field, .. } => {
10068 assert_eq!(field, "session_id");
10069 }
10070 other => panic!("expected the session_id leaf, got {other:?}"),
10071 }
10072 }
10073
10074 #[test]
10075 fn test_goal_state_post_filter() {
10076 let grain = CalGrainResult {
10077 hash: "abc".into(),
10078 grain_type: "goal".into(),
10079 score: 1.0,
10080 fields: serde_json::json!({"goal_state": "active", "subject": "alice"}),
10081 score_breakdown: None,
10082 explanation: None,
10083 relative_time: None,
10084 is_deterministic: false,
10085 contested_by: None,
10086 };
10087
10088 assert!(super::grain_matches_condition(
10089 &grain,
10090 "goal_state",
10091 &super::super::ast::Comparator::Eq,
10092 &super::super::ast::Value::String {
10093 value: "active".into()
10094 }
10095 ));
10096 assert!(!super::grain_matches_condition(
10097 &grain,
10098 "goal_state",
10099 &super::super::ast::Comparator::Eq,
10100 &super::super::ast::Value::String {
10101 value: "failed".into()
10102 }
10103 ));
10104 }
10105
10106 #[test]
10107 fn test_session_id_post_filter() {
10108 let grain = CalGrainResult {
10109 hash: "abc".into(),
10110 grain_type: "event".into(),
10111 score: 1.0,
10112 fields: serde_json::json!({"session_id": "sess-001", "subject": "alice"}),
10113 score_breakdown: None,
10114 explanation: None,
10115 relative_time: None,
10116 is_deterministic: false,
10117 contested_by: None,
10118 };
10119
10120 assert!(super::grain_matches_condition(
10121 &grain,
10122 "session_id",
10123 &super::super::ast::Comparator::Eq,
10124 &super::super::ast::Value::String {
10125 value: "sess-001".into()
10126 }
10127 ));
10128 assert!(!super::grain_matches_condition(
10129 &grain,
10130 "session_id",
10131 &super::super::ast::Comparator::Eq,
10132 &super::super::ast::Value::String {
10133 value: "sess-999".into()
10134 }
10135 ));
10136 }
10137
10138 #[test]
10141 fn event_role_listed_as_type_specific() {
10142 let fields = super::type_specific_fields(&GrainTypePlural::Events);
10143 assert!(fields.contains(&"role"));
10144 assert!(fields.contains(&"parent_message_id"));
10145 assert!(fields.contains(&"model_id"));
10146 assert!(fields.contains(&"stop_reason"));
10147 }
10148
10149 #[test]
10150 fn event_role_not_in_common_fields() {
10151 assert!(!super::COMMON_FIELDS.contains(&"role"));
10152 assert!(!super::COMMON_FIELDS.contains(&"parent_message_id"));
10153 assert!(!super::COMMON_FIELDS.contains(&"model_id"));
10154 assert!(!super::COMMON_FIELDS.contains(&"stop_reason"));
10155 }
10156
10157 #[test]
10158 fn role_eq_post_filter() {
10159 let grain = CalGrainResult {
10160 hash: "h1".into(),
10161 grain_type: "event".into(),
10162 score: 1.0,
10163 fields: serde_json::json!({"role": "user", "content": "hi"}),
10164 score_breakdown: None,
10165 explanation: None,
10166 relative_time: None,
10167 is_deterministic: false,
10168 contested_by: None,
10169 };
10170 assert!(super::grain_matches_condition(
10171 &grain,
10172 "role",
10173 &super::super::ast::Comparator::Eq,
10174 &super::super::ast::Value::String {
10175 value: "user".into()
10176 }
10177 ));
10178 assert!(!super::grain_matches_condition(
10179 &grain,
10180 "role",
10181 &super::super::ast::Comparator::Eq,
10182 &super::super::ast::Value::String {
10183 value: "assistant".into()
10184 }
10185 ));
10186 }
10187
10188 #[test]
10189 fn role_in_list_post_filter() {
10190 use super::super::ast::{Condition, Value};
10191
10192 let condition = Condition::In {
10193 field: "role".into(),
10194 values: vec![
10195 Value::String {
10196 value: "user".into(),
10197 },
10198 Value::String {
10199 value: "assistant".into(),
10200 },
10201 ],
10202 span: None,
10203 };
10204
10205 let user_grain = CalGrainResult {
10206 hash: "h1".into(),
10207 grain_type: "event".into(),
10208 score: 1.0,
10209 fields: serde_json::json!({"role": "user"}),
10210 score_breakdown: None,
10211 explanation: None,
10212 relative_time: None,
10213 is_deterministic: false,
10214 contested_by: None,
10215 };
10216 let tool_grain = CalGrainResult {
10217 hash: "h2".into(),
10218 grain_type: "event".into(),
10219 score: 1.0,
10220 fields: serde_json::json!({"role": "tool"}),
10221 score_breakdown: None,
10222 explanation: None,
10223 relative_time: None,
10224 is_deterministic: false,
10225 contested_by: None,
10226 };
10227
10228 assert!(super::grain_matches_condition_tree(&user_grain, &condition));
10229 assert!(!super::grain_matches_condition_tree(
10230 &tool_grain,
10231 &condition
10232 ));
10233 }
10234
10235 #[test]
10236 fn parent_message_id_eq_post_filter() {
10237 let grain = CalGrainResult {
10238 hash: "h1".into(),
10239 grain_type: "event".into(),
10240 score: 1.0,
10241 fields: serde_json::json!({"parent_message_id": "deadbeef"}),
10242 score_breakdown: None,
10243 explanation: None,
10244 relative_time: None,
10245 is_deterministic: false,
10246 contested_by: None,
10247 };
10248 assert!(super::grain_matches_condition(
10249 &grain,
10250 "parent_message_id",
10251 &super::super::ast::Comparator::Eq,
10252 &super::super::ast::Value::String {
10253 value: "deadbeef".into()
10254 }
10255 ));
10256 }
10257
10258 #[test]
10259 fn role_and_session_id_both_stay_residual() {
10260 use super::super::ast::{Comparator, Condition, Value};
10261 let condition = Condition::And {
10262 left: Box::new(Condition::Comparison {
10263 field: "role".into(),
10264 comparator: Comparator::Eq,
10265 value: Value::String {
10266 value: "user".into(),
10267 },
10268 span: None,
10269 }),
10270 right: Box::new(Condition::Comparison {
10271 field: "session_id".into(),
10272 comparator: Comparator::Eq,
10273 value: Value::String { value: "s1".into() },
10274 span: None,
10275 }),
10276 span: None,
10277 };
10278 let mut warnings = Vec::new();
10279 let residual =
10280 super::plan_residual_where(&condition, &GrainTypePlural::Events, &mut warnings)
10281 .expect("plan must succeed")
10282 .expect("both leaves must remain residual");
10283 match residual {
10286 super::super::ast::Condition::And { .. } => {}
10287 other => panic!("expected the full AND tree, got {other:?}"),
10288 }
10289 }
10290
10291 #[test]
10294 fn test_assemble_where_filters_results() {
10295 let store = MockStore::with_grains(vec![
10296 make_fact("john", "likes", "coffee"),
10297 make_fact("bob", "likes", "coffee"),
10298 ]);
10299 let ex = exec();
10300
10301 let assemble = super::super::ast::AssembleStmt {
10302 topic: "test".into(),
10303 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10304 grain_type: GrainTypePlural::Facts,
10305 about: None,
10306 where_clause: None,
10307 recent: None,
10308 since: None,
10309 until: None,
10310 like: None,
10311 between: None,
10312 contradictions: None,
10313 limit: None,
10314 as_format: None,
10315 span: None,
10316 })),
10317 where_clause: Some(super::super::ast::WhereClause {
10318 condition: super::super::ast::Condition::Comparison {
10319 field: "subject".into(),
10320 comparator: super::super::ast::Comparator::Eq,
10321 value: super::super::ast::Value::String {
10322 value: "john".into(),
10323 },
10324 span: None,
10325 },
10326 span: None,
10327 }),
10328 context_name: None,
10329 sources: None,
10330 budget: None,
10331 priority: None,
10332 format: None,
10333 for_whom: None,
10334 assemble_with: Vec::new(),
10335 with_options: Vec::new(),
10336 streaming: false,
10337 span: None,
10338 };
10339
10340 let query = CalQuery {
10341 version: super::super::ast::CalVersion(1),
10342 statement: CalStatement::Assemble(assemble.clone()),
10343 pipeline: Vec::new(),
10344 with_options: Vec::new(),
10345 format: None,
10346 let_bindings: Vec::new(),
10347 let_values: Default::default(),
10348 user_vars: HashMap::new(),
10349 warnings: Vec::new(),
10350 };
10351
10352 let mut warnings = Vec::new();
10353 let result = ex
10354 .execute_assemble(&assemble, &store, &query, &mut warnings)
10355 .unwrap();
10356
10357 match result {
10358 CalResultPayload::Grains { grains, .. } => {
10359 assert_eq!(grains.len(), 1, "WHERE subject='john' should filter to 1");
10360 assert_eq!(
10361 grains[0].fields.get("subject").and_then(|v| v.as_str()),
10362 Some("john")
10363 );
10364 }
10365 other => panic!("expected Grains, got: {:?}", other),
10366 }
10367 }
10368
10369 #[test]
10370 fn test_assemble_format_json() {
10371 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10372 let ex = exec();
10373
10374 let assemble = super::super::ast::AssembleStmt {
10375 topic: "test".into(),
10376 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10377 grain_type: GrainTypePlural::Facts,
10378 about: None,
10379 where_clause: None,
10380 recent: None,
10381 since: None,
10382 until: None,
10383 like: None,
10384 between: None,
10385 contradictions: None,
10386 limit: None,
10387 as_format: None,
10388 span: None,
10389 })),
10390 where_clause: None,
10391 context_name: None,
10392 sources: None,
10393 budget: None,
10394 priority: None,
10395 format: Some(super::super::ast::FormatClause::Single(
10396 super::super::ast::FormatSpec::Json,
10397 )),
10398 for_whom: None,
10399 assemble_with: Vec::new(),
10400 with_options: Vec::new(),
10401 streaming: false,
10402 span: None,
10403 };
10404
10405 let query = CalQuery {
10406 version: super::super::ast::CalVersion(1),
10407 statement: CalStatement::Assemble(assemble.clone()),
10408 pipeline: Vec::new(),
10409 with_options: Vec::new(),
10410 format: None,
10411 let_bindings: Vec::new(),
10412 let_values: Default::default(),
10413 user_vars: HashMap::new(),
10414 warnings: Vec::new(),
10415 };
10416
10417 let mut warnings = Vec::new();
10418 let result = ex
10419 .execute_assemble(&assemble, &store, &query, &mut warnings)
10420 .unwrap();
10421
10422 match result {
10425 CalResultPayload::Grains { grains, .. } => {
10426 assert_eq!(grains.len(), 1);
10427 assert_eq!(grains[0].grain_type, "fact");
10428 let subject = grains[0].fields.get("subject").and_then(|v| v.as_str());
10429 assert_eq!(subject, Some("john"));
10430 }
10431 other => panic!("expected Grains, got: {:?}", other),
10432 }
10433 }
10434
10435 #[test]
10436 fn test_assemble_format_markdown() {
10437 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10438 let ex = exec();
10439
10440 let assemble = super::super::ast::AssembleStmt {
10441 topic: "test".into(),
10442 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10443 grain_type: GrainTypePlural::Facts,
10444 about: None,
10445 where_clause: None,
10446 recent: None,
10447 since: None,
10448 until: None,
10449 like: None,
10450 between: None,
10451 contradictions: None,
10452 limit: None,
10453 as_format: None,
10454 span: None,
10455 })),
10456 where_clause: None,
10457 context_name: None,
10458 sources: None,
10459 budget: None,
10460 priority: None,
10461 format: Some(super::super::ast::FormatClause::Single(
10462 super::super::ast::FormatSpec::Markdown,
10463 )),
10464 for_whom: None,
10465 assemble_with: Vec::new(),
10466 with_options: Vec::new(),
10467 streaming: false,
10468 span: None,
10469 };
10470
10471 let query = CalQuery {
10472 version: super::super::ast::CalVersion(1),
10473 statement: CalStatement::Assemble(assemble.clone()),
10474 pipeline: Vec::new(),
10475 with_options: Vec::new(),
10476 format: None,
10477 let_bindings: Vec::new(),
10478 let_values: Default::default(),
10479 user_vars: HashMap::new(),
10480 warnings: Vec::new(),
10481 };
10482
10483 let mut warnings = Vec::new();
10484 let result = ex
10485 .execute_assemble(&assemble, &store, &query, &mut warnings)
10486 .unwrap();
10487
10488 match result {
10489 CalResultPayload::Formatted {
10490 text,
10491 format,
10492 grain_count,
10493 ..
10494 } => {
10495 assert_eq!(format, "markdown");
10496 assert_eq!(grain_count, 1);
10497 assert!(
10502 !text.contains("###"),
10503 "ungrouped markdown should not carry per-grain headings, got: {text}"
10504 );
10505 assert!(
10509 text.starts_with("- **"),
10510 "Markdown should render an assertion line, got: {text}"
10511 );
10512 }
10513 other => panic!("expected Formatted, got: {:?}", other),
10514 }
10515 }
10516
10517 #[test]
10518 fn test_assemble_format_triples() {
10519 let store = MockStore::with_grains(vec![
10520 make_fact("john", "likes", "coffee"),
10521 make_fact("bob", "likes", "coffee"),
10522 ]);
10523 let ex = exec();
10524
10525 let assemble = super::super::ast::AssembleStmt {
10526 topic: "test".into(),
10527 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10528 grain_type: GrainTypePlural::Facts,
10529 about: None,
10530 where_clause: None,
10531 recent: None,
10532 since: None,
10533 until: None,
10534 like: None,
10535 between: None,
10536 contradictions: None,
10537 limit: None,
10538 as_format: None,
10539 span: None,
10540 })),
10541 where_clause: None,
10542 context_name: None,
10543 sources: None,
10544 budget: None,
10545 priority: None,
10546 format: Some(super::super::ast::FormatClause::Single(
10547 super::super::ast::FormatSpec::Triples,
10548 )),
10549 for_whom: None,
10550 assemble_with: Vec::new(),
10551 with_options: Vec::new(),
10552 streaming: false,
10553 span: None,
10554 };
10555
10556 let query = CalQuery {
10557 version: super::super::ast::CalVersion(1),
10558 statement: CalStatement::Assemble(assemble.clone()),
10559 pipeline: Vec::new(),
10560 with_options: Vec::new(),
10561 format: None,
10562 let_bindings: Vec::new(),
10563 let_values: Default::default(),
10564 user_vars: HashMap::new(),
10565 warnings: Vec::new(),
10566 };
10567
10568 let mut warnings = Vec::new();
10569 let result = ex
10570 .execute_assemble(&assemble, &store, &query, &mut warnings)
10571 .unwrap();
10572
10573 match result {
10574 CalResultPayload::Formatted {
10575 text,
10576 format,
10577 grain_count,
10578 ..
10579 } => {
10580 assert_eq!(format, "triples");
10581 assert_eq!(grain_count, 2);
10582 assert_eq!(text.lines().count(), 2, "should have 2 triple lines");
10584 }
10585 other => panic!("expected Formatted, got: {:?}", other),
10586 }
10587 }
10588
10589 #[test]
10590 fn test_assemble_format_with_where_filters_then_formats() {
10591 let store = MockStore::with_grains(vec![
10593 make_fact("john", "likes", "coffee"),
10594 make_fact("bob", "likes", "coffee"),
10595 ]);
10596 let ex = exec();
10597
10598 let assemble = super::super::ast::AssembleStmt {
10599 topic: "test".into(),
10600 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10601 grain_type: GrainTypePlural::Facts,
10602 about: None,
10603 where_clause: None,
10604 recent: None,
10605 since: None,
10606 until: None,
10607 like: None,
10608 between: None,
10609 contradictions: None,
10610 limit: None,
10611 as_format: None,
10612 span: None,
10613 })),
10614 where_clause: Some(super::super::ast::WhereClause {
10615 condition: super::super::ast::Condition::Comparison {
10616 field: "subject".into(),
10617 comparator: super::super::ast::Comparator::Eq,
10618 value: super::super::ast::Value::String {
10619 value: "john".into(),
10620 },
10621 span: None,
10622 },
10623 span: None,
10624 }),
10625 context_name: None,
10626 sources: None,
10627 budget: None,
10628 priority: None,
10629 format: Some(super::super::ast::FormatClause::Single(
10630 super::super::ast::FormatSpec::Json,
10631 )),
10632 for_whom: None,
10633 assemble_with: Vec::new(),
10634 with_options: Vec::new(),
10635 streaming: false,
10636 span: None,
10637 };
10638
10639 let query = CalQuery {
10640 version: super::super::ast::CalVersion(1),
10641 statement: CalStatement::Assemble(assemble.clone()),
10642 pipeline: Vec::new(),
10643 with_options: Vec::new(),
10644 format: None,
10645 let_bindings: Vec::new(),
10646 let_values: Default::default(),
10647 user_vars: HashMap::new(),
10648 warnings: Vec::new(),
10649 };
10650
10651 let mut warnings = Vec::new();
10652 let result = ex
10653 .execute_assemble(&assemble, &store, &query, &mut warnings)
10654 .unwrap();
10655
10656 match result {
10658 CalResultPayload::Grains { grains, .. } => {
10659 assert_eq!(grains.len(), 1, "WHERE should filter to 1 grain");
10660 let subject = grains[0].fields.get("subject").and_then(|v| v.as_str());
10661 assert_eq!(subject, Some("john"));
10662 }
10663 other => panic!("expected Grains, got: {:?}", other),
10664 }
10665 }
10666
10667 #[test]
10668 fn test_assemble_without_where_returns_all() {
10669 let store = MockStore::with_grains(vec![
10670 make_fact("john", "likes", "coffee"),
10671 make_fact("bob", "likes", "coffee"),
10672 ]);
10673 let ex = exec();
10674
10675 let assemble = super::super::ast::AssembleStmt {
10676 topic: "test".into(),
10677 from: super::super::ast::Source::Query(Box::new(RecallStmt {
10678 grain_type: GrainTypePlural::Facts,
10679 about: None,
10680 where_clause: None,
10681 recent: None,
10682 since: None,
10683 until: None,
10684 like: None,
10685 between: None,
10686 contradictions: None,
10687 limit: None,
10688 as_format: None,
10689 span: None,
10690 })),
10691 where_clause: None,
10692 context_name: None,
10693 sources: None,
10694 budget: None,
10695 priority: None,
10696 format: None,
10697 for_whom: None,
10698 assemble_with: Vec::new(),
10699 with_options: Vec::new(),
10700 streaming: false,
10701 span: None,
10702 };
10703
10704 let query = CalQuery {
10705 version: super::super::ast::CalVersion(1),
10706 statement: CalStatement::Assemble(assemble.clone()),
10707 pipeline: Vec::new(),
10708 with_options: Vec::new(),
10709 format: None,
10710 let_bindings: Vec::new(),
10711 let_values: Default::default(),
10712 user_vars: HashMap::new(),
10713 warnings: Vec::new(),
10714 };
10715
10716 let mut warnings = Vec::new();
10717 let result = ex
10718 .execute_assemble(&assemble, &store, &query, &mut warnings)
10719 .unwrap();
10720
10721 match result {
10722 CalResultPayload::Grains { grains, .. } => {
10723 assert_eq!(grains.len(), 2, "no WHERE should return all");
10724 }
10725 other => panic!("expected Grains, got: {:?}", other),
10726 }
10727 }
10728
10729 #[test]
10732 fn test_multisource_assemble_format_json() {
10733 let store = MockStore::with_grains(vec![
10734 make_fact("john", "likes", "coffee"),
10735 make_fact("bob", "likes", "coffee"),
10736 ]);
10737 let ex = exec();
10738
10739 let recall_facts = RecallStmt {
10740 grain_type: GrainTypePlural::Facts,
10741 about: None,
10742 where_clause: None,
10743 recent: None,
10744 since: None,
10745 until: None,
10746 like: None,
10747 between: None,
10748 contradictions: None,
10749 limit: None,
10750 as_format: None,
10751 span: None,
10752 };
10753
10754 let assemble = super::super::ast::AssembleStmt {
10755 topic: "test".into(),
10756 from: super::super::ast::Source::Query(Box::new(recall_facts.clone())),
10757 where_clause: None,
10758 context_name: None,
10759 sources: Some(vec![super::super::ast::NamedSource {
10760 label: "facts".into(),
10761 literal: None,
10762 pinned: false,
10763 query: Box::new(CalStatement::Recall(recall_facts)),
10764 with_options: vec![],
10765 span: None,
10766 }]),
10767 budget: None,
10768 priority: None,
10769 format: Some(super::super::ast::FormatClause::Single(
10770 super::super::ast::FormatSpec::Json,
10771 )),
10772 for_whom: None,
10773 assemble_with: Vec::new(),
10774 with_options: Vec::new(),
10775 streaming: false,
10776 span: None,
10777 };
10778
10779 let query = CalQuery {
10780 version: super::super::ast::CalVersion(1),
10781 statement: CalStatement::Assemble(assemble.clone()),
10782 pipeline: Vec::new(),
10783 with_options: Vec::new(),
10784 format: None,
10785 let_bindings: Vec::new(),
10786 let_values: Default::default(),
10787 user_vars: HashMap::new(),
10788 warnings: Vec::new(),
10789 };
10790
10791 let mut warnings = Vec::new();
10792 let result = ex
10793 .execute_assemble(&assemble, &store, &query, &mut warnings)
10794 .unwrap();
10795
10796 match result {
10798 CalResultPayload::Grains { grains, .. } => {
10799 assert!(!grains.is_empty(), "should have grains");
10800 assert!(
10801 grains
10802 .iter()
10803 .filter_map(|g| g.fields.get("subject").and_then(|v| v.as_str()))
10804 .any(|x| x == "john"),
10805 "should contain john"
10806 );
10807 }
10808 other => panic!(
10809 "expected Grains for multi-source FORMAT json, got: {:?}",
10810 other
10811 ),
10812 }
10813 }
10814
10815 #[test]
10816 fn test_multisource_assemble_format_sml() {
10817 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10818 let ex = exec();
10819
10820 let recall_facts = RecallStmt {
10821 grain_type: GrainTypePlural::Facts,
10822 about: None,
10823 where_clause: None,
10824 recent: None,
10825 since: None,
10826 until: None,
10827 like: None,
10828 between: None,
10829 contradictions: None,
10830 limit: None,
10831 as_format: None,
10832 span: None,
10833 };
10834
10835 let assemble = super::super::ast::AssembleStmt {
10836 topic: "test".into(),
10837 from: super::super::ast::Source::Query(Box::new(recall_facts.clone())),
10838 where_clause: None,
10839 context_name: None,
10840 sources: Some(vec![super::super::ast::NamedSource {
10841 label: "events".into(),
10842 literal: None,
10843 pinned: false,
10844 query: Box::new(CalStatement::Recall(recall_facts)),
10845 with_options: vec![],
10846 span: None,
10847 }]),
10848 budget: None,
10849 priority: None,
10850 format: Some(super::super::ast::FormatClause::Single(
10851 super::super::ast::FormatSpec::Sml,
10852 )),
10853 for_whom: None,
10854 assemble_with: Vec::new(),
10855 with_options: Vec::new(),
10856 streaming: false,
10857 span: None,
10858 };
10859
10860 let query = CalQuery {
10861 version: super::super::ast::CalVersion(1),
10862 statement: CalStatement::Assemble(assemble.clone()),
10863 pipeline: Vec::new(),
10864 with_options: Vec::new(),
10865 format: None,
10866 let_bindings: Vec::new(),
10867 let_values: Default::default(),
10868 user_vars: HashMap::new(),
10869 warnings: Vec::new(),
10870 };
10871
10872 let mut warnings = Vec::new();
10873 let result = ex
10874 .execute_assemble(&assemble, &store, &query, &mut warnings)
10875 .unwrap();
10876
10877 match result {
10878 CalResultPayload::Formatted {
10879 text,
10880 format,
10881 grain_count,
10882 ..
10883 } => {
10884 assert_eq!(format, "sml");
10885 assert!(grain_count > 0, "should have grains");
10886 assert!(text.contains("<grains>"), "SML should contain <grains> tag");
10887 }
10888 other => panic!(
10889 "expected Formatted for multi-source FORMAT sml, got: {:?}",
10890 other
10891 ),
10892 }
10893 }
10894
10895 #[test]
10896 fn test_multisource_assemble_without_format_returns_assembled() {
10897 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10898 let ex = exec();
10899
10900 let recall_facts = RecallStmt {
10901 grain_type: GrainTypePlural::Facts,
10902 about: None,
10903 where_clause: None,
10904 recent: None,
10905 since: None,
10906 until: None,
10907 like: None,
10908 between: None,
10909 contradictions: None,
10910 limit: None,
10911 as_format: None,
10912 span: None,
10913 };
10914
10915 let assemble = super::super::ast::AssembleStmt {
10916 topic: "test".into(),
10917 from: super::super::ast::Source::Query(Box::new(recall_facts.clone())),
10918 where_clause: None,
10919 context_name: None,
10920 sources: Some(vec![super::super::ast::NamedSource {
10921 label: "facts".into(),
10922 literal: None,
10923 pinned: false,
10924 query: Box::new(CalStatement::Recall(recall_facts)),
10925 with_options: vec![],
10926 span: None,
10927 }]),
10928 budget: None,
10929 priority: None,
10930 format: None,
10931 for_whom: None,
10932 assemble_with: Vec::new(),
10933 with_options: Vec::new(),
10934 streaming: false,
10935 span: None,
10936 };
10937
10938 let query = CalQuery {
10939 version: super::super::ast::CalVersion(1),
10940 statement: CalStatement::Assemble(assemble.clone()),
10941 pipeline: Vec::new(),
10942 with_options: Vec::new(),
10943 format: None,
10944 let_bindings: Vec::new(),
10945 let_values: Default::default(),
10946 user_vars: HashMap::new(),
10947 warnings: Vec::new(),
10948 };
10949
10950 let mut warnings = Vec::new();
10951 let result = ex
10952 .execute_assemble(&assemble, &store, &query, &mut warnings)
10953 .unwrap();
10954
10955 match result {
10956 CalResultPayload::Assembled { grains, .. } => {
10957 assert!(!grains.is_empty(), "should have assembled grains");
10958 }
10959 other => panic!("expected Assembled (no FORMAT), got: {:?}", other),
10960 }
10961 }
10962
10963 #[test]
10966 fn test_multi_format_rendering() {
10967 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10968 let ex = CalExecutor::with_defaults();
10969
10970 let result = ex
10971 .execute("RECALL facts FORMAT [json, markdown]", &store)
10972 .unwrap();
10973 match result.result {
10974 CalResultPayload::MultiFormatted {
10975 formats,
10976 grain_count,
10977 ..
10978 } => {
10979 assert_eq!(grain_count, 1);
10980 assert_eq!(formats.len(), 2);
10981 assert!(formats.contains_key("json"), "should have json key");
10982 assert!(formats.contains_key("markdown"), "should have markdown key");
10983 assert!(formats["json"].contains("john"));
10985 assert!(formats["markdown"].contains("john"));
10987 }
10988 other => panic!("expected MultiFormatted, got: {:?}", other),
10989 }
10990 }
10991
10992 #[test]
10993 fn test_single_format_backward_compat() {
10994 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
10995 let ex = CalExecutor::with_defaults();
10996
10997 let result = ex.execute("RECALL facts FORMAT json", &store).unwrap();
10999 match result.result {
11000 CalResultPayload::Grains { grains, .. } => {
11001 assert_eq!(grains.len(), 1);
11002 }
11003 other => panic!("expected Grains, got: {:?}", other),
11004 }
11005 }
11006
11007 #[test]
11008 fn test_multi_format_single_element_list() {
11009 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11010 let ex = CalExecutor::with_defaults();
11011
11012 let result = ex.execute("RECALL facts FORMAT [json]", &store).unwrap();
11014 match result.result {
11015 CalResultPayload::MultiFormatted {
11016 formats,
11017 grain_count,
11018 ..
11019 } => {
11020 assert_eq!(grain_count, 1);
11021 assert_eq!(formats.len(), 1);
11022 assert!(formats.contains_key("json"));
11023 }
11024 other => panic!("expected MultiFormatted, got: {:?}", other),
11025 }
11026 }
11027
11028 #[test]
11029 fn test_multi_format_all_seven_types() {
11030 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11031 let ex = CalExecutor::with_defaults();
11032
11033 let result = ex
11034 .execute(
11035 "RECALL facts FORMAT [json, markdown, yaml, text, sml]",
11036 &store,
11037 )
11038 .unwrap();
11039 match result.result {
11040 CalResultPayload::MultiFormatted { formats, .. } => {
11041 assert_eq!(formats.len(), 5);
11042 for key in &["json", "markdown", "yaml", "text", "sml"] {
11043 assert!(formats.contains_key(*key), "missing format: {}", key);
11044 }
11045 }
11046 other => panic!("expected MultiFormatted, got: {:?}", other),
11047 }
11048 }
11049
11050 #[test]
11051 fn test_no_format_returns_grains() {
11052 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11053 let ex = CalExecutor::with_defaults();
11054
11055 let result = ex.execute("RECALL facts", &store).unwrap();
11057 assert!(matches!(result.result, CalResultPayload::Grains { .. }));
11058 }
11059
11060 #[test]
11061 fn test_multi_format_with_aliases() {
11062 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11063 let ex = CalExecutor::with_defaults();
11064
11065 let result = ex
11066 .execute(
11067 "RECALL facts FORMAT [json AS customers, markdown AS report]",
11068 &store,
11069 )
11070 .unwrap();
11071 match result.result {
11072 CalResultPayload::MultiFormatted {
11073 formats,
11074 grain_count,
11075 ..
11076 } => {
11077 assert_eq!(grain_count, 1);
11078 assert_eq!(formats.len(), 2);
11079 assert!(
11080 formats.contains_key("customers"),
11081 "should have aliased key 'customers'"
11082 );
11083 assert!(
11084 formats.contains_key("report"),
11085 "should have aliased key 'report'"
11086 );
11087 assert!(
11088 !formats.contains_key("json"),
11089 "should NOT have canonical key 'json'"
11090 );
11091 assert!(
11092 !formats.contains_key("markdown"),
11093 "should NOT have canonical key 'markdown'"
11094 );
11095 assert!(formats["customers"].contains("john"));
11096 assert!(formats["report"].contains("john"));
11097 }
11098 other => panic!("expected MultiFormatted, got: {:?}", other),
11099 }
11100 }
11101
11102 #[test]
11103 fn test_multi_format_mixed_alias_and_no_alias() {
11104 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11105 let ex = CalExecutor::with_defaults();
11106
11107 let result = ex
11108 .execute("RECALL facts FORMAT [json AS customers, markdown]", &store)
11109 .unwrap();
11110 match result.result {
11111 CalResultPayload::MultiFormatted {
11112 formats,
11113 grain_count,
11114 ..
11115 } => {
11116 assert_eq!(grain_count, 1);
11117 assert_eq!(formats.len(), 2);
11118 assert!(formats.contains_key("customers"), "aliased key");
11119 assert!(
11120 formats.contains_key("markdown"),
11121 "canonical key for non-aliased"
11122 );
11123 }
11124 other => panic!("expected MultiFormatted, got: {:?}", other),
11125 }
11126 }
11127
11128 #[test]
11129 fn test_multi_format_template_alias() {
11130 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11131 let ex = CalExecutor::with_defaults();
11132
11133 let result = ex
11134 .execute(
11135 r#"RECALL facts FORMAT [TEMPLATE "{{subject}}: {{object}}" AS summary, json]"#,
11136 &store,
11137 )
11138 .unwrap();
11139 match result.result {
11140 CalResultPayload::MultiFormatted {
11141 formats,
11142 grain_count,
11143 ..
11144 } => {
11145 assert_eq!(grain_count, 1);
11146 assert_eq!(formats.len(), 2);
11147 assert!(formats.contains_key("summary"), "template alias");
11148 assert!(formats.contains_key("json"), "json canonical");
11149 assert!(formats["summary"].contains("john: coffee"));
11150 }
11151 other => panic!("expected MultiFormatted, got: {:?}", other),
11152 }
11153 }
11154
11155 fn make_fact_with_time(
11159 subject: &str,
11160 relation: &str,
11161 object: &str,
11162 created_at_sec: u32,
11163 ) -> (Hash, DeserializedGrain) {
11164 let mut fields: HashMap<String, serde_json::Value> = HashMap::new();
11165 fields.insert("subject".into(), serde_json::json!(subject));
11166 fields.insert("relation".into(), serde_json::json!(relation));
11167 fields.insert("object".into(), serde_json::json!(object));
11168 fields.insert("grain_type".into(), serde_json::json!("fact"));
11169 fields.insert("confidence".into(), serde_json::json!(0.9));
11170 fields.insert("created_at_sec".into(), serde_json::json!(created_at_sec));
11171
11172 let mut hash_bytes = [0u8; 32];
11173 let key = format!("{}|{}|{}|{}", subject, relation, object, created_at_sec);
11174 for (i, b) in key.as_bytes().iter().enumerate().take(32) {
11175 hash_bytes[i] = *b;
11176 }
11177 let hash = Hash::from_bytes(&hash_bytes);
11178
11179 let grain = DeserializedGrain {
11180 header: MgHeader {
11181 version: 1,
11182 flags: 0,
11183 grain_type: 0x01,
11184 ns_hash: 0,
11185 created_at_sec,
11186 },
11187 grain_type: GrainType::Fact,
11188 fields,
11189 hash,
11190 };
11191 (hash, grain)
11192 }
11193
11194 #[test]
11195 fn test_group_by_parses() {
11196 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11197 let ex = CalExecutor::with_defaults();
11198 let result = ex.execute("RECALL facts GROUP BY subject", &store);
11199 assert!(result.is_ok(), "GROUP BY should parse and execute");
11200 }
11201
11202 #[test]
11203 fn test_group_by_reorders_grains() {
11204 let store = MockStore::with_grains(vec![
11206 make_fact_with_time("bob", "likes", "tea", 100),
11207 make_fact_with_time("john", "likes", "coffee", 200),
11208 make_fact_with_time("bob", "likes", "vim", 300),
11209 make_fact_with_time("john", "likes", "rust", 400),
11210 ]);
11211 let ex = CalExecutor::with_defaults();
11212
11213 let result = ex.execute("RECALL facts GROUP BY subject", &store).unwrap();
11214 match result.result {
11215 CalResultPayload::Grains { grains, .. } => {
11216 assert_eq!(grains.len(), 4);
11217 let subjects: Vec<&str> = grains
11221 .iter()
11222 .filter_map(|g| g.fields.get("subject").and_then(|v| v.as_str()))
11223 .collect();
11224 assert_eq!(subjects, vec!["bob", "bob", "john", "john"]);
11225 }
11226 other => panic!("expected Grains, got: {:?}", other),
11227 }
11228 }
11229
11230 #[test]
11231 fn test_group_by_chronological_within_group() {
11232 let store = MockStore::with_grains(vec![
11233 make_fact_with_time("john", "likes", "vim", 500),
11234 make_fact_with_time("john", "likes", "coffee", 100),
11235 make_fact_with_time("john", "likes", "rust", 300),
11236 ]);
11237 let ex = CalExecutor::with_defaults();
11238
11239 let result = ex.execute("RECALL facts GROUP BY subject", &store).unwrap();
11240 match result.result {
11241 CalResultPayload::Grains { grains, .. } => {
11242 let times: Vec<u64> = grains
11243 .iter()
11244 .filter_map(|g| g.fields.get("created_at_sec").and_then(|v| v.as_u64()))
11245 .collect();
11246 assert_eq!(
11247 times,
11248 vec![100, 300, 500],
11249 "should be chronological within group"
11250 );
11251 }
11252 other => panic!("expected Grains, got: {:?}", other),
11253 }
11254 }
11255
11256 #[test]
11257 fn test_group_by_sml_format() {
11258 let store = MockStore::with_grains(vec![
11259 make_fact_with_time("sess_1", "user", "hello", 100),
11260 make_fact_with_time("sess_2", "user", "world", 200),
11261 make_fact_with_time("sess_1", "assistant", "hi", 150),
11262 ]);
11263 let ex = CalExecutor::with_defaults();
11264
11265 let result = ex
11266 .execute("RECALL facts GROUP BY subject FORMAT sml", &store)
11267 .unwrap();
11268 match result.result {
11269 CalResultPayload::Formatted { text, format, .. } => {
11270 assert_eq!(format, "sml");
11271 assert!(text.contains("<group key=\"sess_1\" count=\"2\">"));
11272 assert!(text.contains("<group key=\"sess_2\" count=\"1\">"));
11273 assert!(text.contains("</group>"));
11274 }
11275 other => panic!("expected Formatted, got: {:?}", other),
11276 }
11277 }
11278
11279 #[test]
11280 fn test_group_by_markdown_format() {
11281 let store = MockStore::with_grains(vec![
11282 make_fact_with_time("sess_1", "user", "hello", 100),
11283 make_fact_with_time("sess_2", "user", "world", 200),
11284 ]);
11285 let ex = CalExecutor::with_defaults();
11286
11287 let result = ex
11288 .execute("RECALL facts GROUP BY subject FORMAT markdown", &store)
11289 .unwrap();
11290 match result.result {
11291 CalResultPayload::Formatted { text, format, .. } => {
11292 assert_eq!(format, "markdown");
11293 assert!(text.contains("### sess_1 (1 memory)"));
11294 assert!(text.contains("### sess_2 (1 memory)"));
11295 }
11296 other => panic!("expected Formatted, got: {:?}", other),
11297 }
11298 }
11299
11300 #[test]
11301 fn test_group_by_text_format() {
11302 let store = MockStore::with_grains(vec![
11303 make_fact_with_time("sess_1", "user", "hello", 100),
11304 make_fact_with_time("sess_1", "assistant", "hi", 150),
11305 make_fact_with_time("sess_2", "user", "world", 200),
11306 ]);
11307 let ex = CalExecutor::with_defaults();
11308
11309 let result = ex
11310 .execute("RECALL facts GROUP BY subject FORMAT text", &store)
11311 .unwrap();
11312 match result.result {
11313 CalResultPayload::Formatted { text, format, .. } => {
11314 assert_eq!(format, "text");
11315 assert!(text.contains("--- Group 1/2: sess_1 (2 memories) ---"));
11316 assert!(text.contains("--- Group 2/2: sess_2 (1 memory) ---"));
11317 assert!(text.contains("[1]"));
11318 assert!(text.contains("[2]"));
11319 }
11320 other => panic!("expected Formatted, got: {:?}", other),
11321 }
11322 }
11323
11324 #[test]
11325 fn test_group_by_json_format() {
11326 let store = MockStore::with_grains(vec![
11327 make_fact_with_time("sess_1", "user", "hello", 100),
11328 make_fact_with_time("sess_2", "user", "world", 200),
11329 ]);
11330 let ex = CalExecutor::with_defaults();
11331
11332 let result = ex
11333 .execute("RECALL facts GROUP BY subject FORMAT json", &store)
11334 .unwrap();
11335 match result.result {
11336 CalResultPayload::Formatted { text, format, .. } => {
11337 assert_eq!(format, "json");
11338 assert!(text.contains("\"group_key\""));
11339 assert!(text.contains("\"sess_1\""));
11340 assert!(text.contains("\"sess_2\""));
11341 assert!(text.contains("\"count\""));
11342 }
11343 other => panic!("expected Formatted, got: {:?}", other),
11344 }
11345 }
11346
11347 #[test]
11348 fn test_group_by_missing_field_present_field() {
11349 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11352 let ex = CalExecutor::with_defaults();
11353
11354 let result = ex
11355 .execute("RECALL facts GROUP BY relation FORMAT text", &store)
11356 .unwrap();
11357 match result.result {
11358 CalResultPayload::Formatted { text, format, .. } => {
11359 assert_eq!(format, "text");
11360 assert!(text.contains("--- Group 1/1:"));
11361 }
11362 other => panic!("expected Formatted, got: {:?}", other),
11363 }
11364 }
11365
11366 #[test]
11367 fn test_group_by_unknown_field_rejected() {
11368 let store = MockStore::with_grains(vec![make_fact("john", "likes", "coffee")]);
11372 let ex = CalExecutor::with_defaults();
11373
11374 let err = ex
11375 .execute("RECALL facts GROUP BY session_id FORMAT text", &store)
11376 .expect_err("GROUP BY unknown field must be rejected per Bug 9");
11377 assert_eq!(err.code(), "CAL-E060");
11378 }
11379
11380 #[test]
11385 fn test_with_vars_template_substitution() {
11386 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
11387 let store = MockStore::with_grains(vec![(hash_a, grain_a)]);
11388 let ex = CalExecutor::with_defaults();
11389
11390 let result = ex
11391 .execute(
11392 r#"RECALL facts FORMAT TEMPLATE "User: {{$user_name}} | {{subject}} {{relation}} {{object}}" WITH VARS { "user_name": "John" }"#,
11393 &store,
11394 )
11395 .unwrap();
11396 match result.result {
11397 CalResultPayload::Formatted { text, format, .. } => {
11398 assert_eq!(format, "template");
11399 assert!(
11400 text.contains("User: John |"),
11401 "user var not substituted: {}",
11402 text
11403 );
11404 assert!(text.contains("john"), "subject not substituted: {}", text);
11405 assert!(text.contains("coffee"), "object not substituted: {}", text);
11406 }
11407 other => panic!("expected Formatted, got: {:?}", other),
11408 }
11409 }
11410
11411 #[test]
11412 fn test_with_vars_missing_var_renders_empty() {
11413 let (hash_a, grain_a) = make_fact("john", "likes", "coffee");
11414 let store = MockStore::with_grains(vec![(hash_a, grain_a)]);
11415 let ex = CalExecutor::with_defaults();
11416
11417 let result = ex
11418 .execute(
11419 r#"RECALL facts FORMAT TEMPLATE "{{$missing}} | {{subject}}" WITH VARS { "other": "val" }"#,
11420 &store,
11421 )
11422 .unwrap();
11423 match result.result {
11424 CalResultPayload::Formatted { text, .. } => {
11425 assert!(
11427 text.contains("john"),
11428 "subject should be resolved: {}",
11429 text
11430 );
11431 }
11432 other => panic!("expected Formatted, got: {:?}", other),
11433 }
11434 }
11435
11436 #[test]
11437 fn test_with_vars_parsed_into_query() {
11438 let store = MockStore::empty();
11439 let ex = CalExecutor::with_defaults();
11440
11441 let result = ex
11442 .execute(
11443 r#"RECALL facts WITH VARS { "app": "test", "version": "1.0" }"#,
11444 &store,
11445 )
11446 .unwrap();
11447 assert_eq!(result.metadata.statement_type, "recall");
11449 }
11450
11451 fn make_event_grain_result(content: &str, relation: &str, subject: &str) -> CalGrainResult {
11455 let mut fields = serde_json::Map::new();
11456 fields.insert("subject".into(), serde_json::json!(subject));
11457 fields.insert("relation".into(), serde_json::json!(relation));
11458 fields.insert("content".into(), serde_json::json!(content));
11459 CalGrainResult {
11460 hash: "aabbccdd00112233".into(),
11461 grain_type: "event".into(),
11462 score: 1.0,
11463 fields: serde_json::Value::Object(fields),
11464 score_breakdown: None,
11465 explanation: None,
11466 relative_time: None,
11467 is_deterministic: false,
11468 contested_by: None,
11469 }
11470 }
11471
11472 fn make_event_grain_result_no_relation(content: &str, subject: &str) -> CalGrainResult {
11474 let mut fields = serde_json::Map::new();
11475 fields.insert("subject".into(), serde_json::json!(subject));
11476 fields.insert("content".into(), serde_json::json!(content));
11477 CalGrainResult {
11478 hash: "aabbccdd00112233".into(),
11479 grain_type: "event".into(),
11480 score: 1.0,
11481 fields: serde_json::Value::Object(fields),
11482 score_breakdown: None,
11483 explanation: None,
11484 relative_time: None,
11485 is_deterministic: false,
11486 contested_by: None,
11487 }
11488 }
11489
11490 fn view_of(grain: &CalGrainResult) -> crate::render::GrainView<'_> {
11492 grain_view(grain)
11493 }
11494
11495 #[test]
11496 fn test_render_sml_event_carries_speaker_role() {
11497 let grain = make_event_grain_result("I am a hair stylist.", "user", "s000");
11498 let out = crate::render::render_grain_sml(&view_of(&grain), crate::render::MetadataDetail::Minimal);
11499 assert!(
11500 out.contains(r#"role="user""#),
11501 "SML should carry the speaker as a role attribute, got: {out}"
11502 );
11503 assert!(
11504 out.starts_with("<event"),
11505 "events render as semantic <event> elements, got: {out}"
11506 );
11507 assert!(out.contains("I am a hair stylist."));
11508 }
11509
11510 #[test]
11511 fn test_render_sml_omits_role_attribute_when_absent() {
11512 let grain = make_event_grain_result_no_relation("Hello world", "s000");
11513 let out = crate::render::render_grain_sml(&view_of(&grain), crate::render::MetadataDetail::Minimal);
11514 assert!(
11515 !out.contains("role="),
11516 "SML should not have a role attribute when the field is absent, got: {out}"
11517 );
11518 }
11519
11520 #[test]
11521 fn test_render_sml_omits_role_attribute_when_empty() {
11522 let grain = make_event_grain_result("Hello world", "", "s000");
11523 let out = crate::render::render_grain_sml(&view_of(&grain), crate::render::MetadataDetail::Minimal);
11524 assert!(
11525 !out.contains("role=\"\""),
11526 "SML should not have an empty role attribute, got: {out}"
11527 );
11528 }
11529
11530 #[test]
11531 fn test_render_sml_escapes_role_attribute() {
11532 let grain = make_event_grain_result("test", "user<script>", "s000");
11533 let out = crate::render::render_grain_sml(&view_of(&grain), crate::render::MetadataDetail::Minimal);
11534 assert!(
11535 out.contains("role=\"user<script>\""),
11536 "SML should escape the role attribute value, got: {out}"
11537 );
11538 }
11539
11540 #[test]
11541 fn test_render_markdown_prefixes_content_with_relation() {
11542 let grain = make_event_grain_result("I am a hair stylist.", "user", "s000");
11543 let out = crate::render::render_grain_markdown(&view_of(&grain));
11544 assert!(
11545 out.contains("- **user**: I am a hair stylist."),
11546 "Markdown should prefix content with relation, got: {out}"
11547 );
11548 }
11549
11550 #[test]
11551 fn test_render_markdown_no_relation_names_the_subject() {
11552 let grain = make_event_grain_result_no_relation("Hello world", "s000");
11553 let out = crate::render::render_grain_markdown(&view_of(&grain));
11554 assert!(
11558 out.contains("- **s000**: Hello world"),
11559 "Markdown should name the subject when there is no relation, got: {out}"
11560 );
11561 }
11562
11563 #[test]
11564 fn test_render_markdown_speaker_disambiguation() {
11565 let assistant = make_event_grain_result("I am a school teacher.", "assistant", "s000");
11567 let user = make_event_grain_result("I am a hair stylist.", "user", "s000");
11568 let mut out = String::new();
11569 out.push_str(&crate::render::render_grain_markdown(&view_of(&assistant)));
11570 out.push_str(&crate::render::render_grain_markdown(&view_of(&user)));
11571 assert!(
11572 out.contains("- **assistant**: I am a school teacher."),
11573 "Markdown should show assistant role, got: {out}"
11574 );
11575 assert!(
11576 out.contains("- **user**: I am a hair stylist."),
11577 "Markdown should show user role, got: {out}"
11578 );
11579 }
11580
11581 #[test]
11582 fn test_render_text_event_with_subject_relation_uses_triple_path() {
11583 let grain = make_event_grain_result("I am a hair stylist.", "user", "s000");
11586 let out = crate::render::render_grain_text_line(&view_of(&grain), None);
11587 assert!(
11588 out.contains("s000 user"),
11589 "Text should render subject + relation via triple path, got: {out}"
11590 );
11591 }
11592
11593 #[test]
11594 fn test_render_text_content_only_no_relation() {
11595 let grain = make_event_grain_result_no_relation("Hello world", "");
11597 let out = crate::render::render_grain_text_line(&view_of(&grain), None);
11598 assert_eq!(
11599 out.trim(),
11600 "Hello world",
11601 "Text should render content without prefix when no triple fields"
11602 );
11603 }
11604
11605 #[test]
11606 fn test_render_text_speaker_disambiguation_via_triple() {
11607 let assistant = make_event_grain_result("I am a school teacher.", "assistant", "s000");
11609 let user = make_event_grain_result("I am a hair stylist.", "user", "s000");
11610 let mut out = String::new();
11611 out.push_str(&crate::render::render_grain_text_line(&view_of(&assistant), Some(1)));
11612 out.push_str(&crate::render::render_grain_text_line(&view_of(&user), Some(2)));
11613 assert!(
11614 out.contains("[1] s000 assistant"),
11615 "Text should show assistant relation in triple, got: {out}"
11616 );
11617 assert!(
11618 out.contains("[2] s000 user"),
11619 "Text should show user relation in triple, got: {out}"
11620 );
11621 }
11622
11623 #[test]
11624 fn test_render_sml_fact_states_the_triple_inline() {
11625 let (_, grain) = make_fact("john", "likes", "coffee");
11627 let cgr = CalGrainResult {
11628 hash: grain.hash.to_hex(),
11629 grain_type: "fact".into(),
11630 score: 1.0,
11631 fields: serde_json::to_value(&grain.fields).unwrap(),
11632 score_breakdown: None,
11633 explanation: None,
11634 relative_time: None,
11635 is_deterministic: false,
11636 contested_by: None,
11637 };
11638 let out = crate::render::render_grain_sml(&view_of(&cgr), crate::render::MetadataDetail::Minimal);
11639 assert!(
11640 out.starts_with("<fact") && out.contains("john likes coffee"),
11641 "SML fact should state the triple inline, got: {out}"
11642 );
11643 }
11644
11645 #[test]
11650 fn test_scope_read_allows_recall() {
11651 let store = MockStore::empty();
11652 let config = CalExecutorConfig {
11653 caller_scopes: vec!["read".to_string()],
11654 ..Default::default()
11655 };
11656 let ex = CalExecutor::new(config);
11657 let result = ex.execute("RECALL facts LIMIT 1", &store);
11658 assert!(result.is_ok(), "read scope should allow RECALL");
11659 }
11660
11661 #[test]
11662 fn test_scope_read_blocks_add() {
11663 let store = MockStore::empty();
11664 let config = CalExecutorConfig {
11665 caller_scopes: vec!["read".to_string()],
11666 ..Default::default()
11667 };
11668 let ex = CalExecutor::new(config);
11669 let result = ex.execute(
11670 r#"ADD fact SET subject = "x" SET relation = "y" SET object = "z" REASON "test""#,
11671 &store,
11672 );
11673 assert!(result.is_err(), "read scope should block ADD");
11674 let err = result.unwrap_err();
11675 assert_eq!(err.code(), "CAL-E114");
11676 }
11677
11678 #[test]
11679 fn test_scope_write_allows_add() {
11680 let store = MockStore::empty();
11681 let config = CalExecutorConfig {
11682 caller_scopes: vec!["read".to_string(), "write".to_string()],
11683 ..Default::default()
11684 };
11685 let ex = CalExecutor::new(config);
11686 let result = ex.execute(
11687 r#"ADD fact SET subject = "x" SET relation = "y" SET object = "z" REASON "test""#,
11688 &store,
11689 );
11690 if let Err(ref e) = result {
11691 assert_ne!(e.code(), "CAL-E114", "write scope should not block ADD");
11692 }
11693 }
11694
11695 #[test]
11696 fn test_forget_hash_parses_user_scope_rejected() {
11697 let store = MockStore::empty();
11702 let ex = CalExecutor::new(CalExecutorConfig {
11704 allow_destructive_ops: true,
11705 ..Default::default()
11706 });
11707
11708 let hash = format!("sha256:{}", "a".repeat(64));
11710 ex.execute(&format!("FORGET {hash}"), &store)
11711 .expect("FORGET <hash> is a valid CAL statement");
11712
11713 let err = ex
11715 .execute("FORGET USER \"test\"", &store)
11716 .expect_err("FORGET USER is not reachable from CAL text");
11717 assert_eq!(err.code(), "CAL-E002");
11718 }
11719
11720 #[test]
11721 fn test_forget_requires_admin_scope_when_scoped() {
11722 let store = MockStore::empty();
11726 let hash = format!("sha256:{}", "a".repeat(64));
11727 let ex = CalExecutor::new(CalExecutorConfig {
11728 caller_scopes: vec!["read".to_string(), "write".to_string()],
11729 allow_destructive_ops: true,
11730 ..Default::default()
11731 });
11732 let err = ex
11733 .execute(&format!("FORGET {hash}"), &store)
11734 .expect_err("FORGET needs admin scope");
11735 assert!(
11736 matches!(err, CalError::InsufficientScope { .. }),
11737 "expected InsufficientScope, got {err:?}"
11738 );
11739 }
11740
11741 #[test]
11742 fn test_forget_hash_gated_by_allow_destructive_ops() {
11743 let store = MockStore::empty();
11746 let hash = format!("sha256:{}", "a".repeat(64));
11747 let ex = CalExecutor::new(CalExecutorConfig {
11748 allow_destructive_ops: false,
11749 ..Default::default()
11750 });
11751 let res = ex
11752 .execute(&format!("FORGET {hash}"), &store)
11753 .expect("FORGET <hash> parses even when disabled");
11754 match res.result {
11755 CalResultPayload::Unsupported { statement, message } => {
11756 assert_eq!(statement, "forget");
11757 assert!(message.contains("disabled"), "unexpected message: {message}");
11758 }
11759 other => panic!("expected Unsupported when disabled, got {other:?}"),
11760 }
11761 }
11762
11763 #[test]
11764 fn test_admin_forget_still_rejected_at_parse_time() {
11765 let store = MockStore::empty();
11769 let config = CalExecutorConfig {
11770 caller_scopes: vec!["read".to_string(), "write".to_string(), "admin".to_string()],
11771 allow_destructive_ops: true,
11772 ..Default::default()
11773 };
11774 let ex = CalExecutor::new(config);
11775 let err = ex
11776 .execute("FORGET USER \"nonexistent\"", &store)
11777 .expect_err("FORGET rejected unconditionally");
11778 assert_eq!(err.code(), "CAL-E002");
11779 }
11780
11781 #[test]
11782 fn test_scope_empty_no_enforcement() {
11783 let store = MockStore::empty();
11784 let config = CalExecutorConfig {
11785 caller_scopes: vec![], ..Default::default()
11787 };
11788 let ex = CalExecutor::new(config);
11789 let result = ex.execute(
11790 r#"ADD fact SET subject = "x" SET relation = "y" SET object = "z" REASON "test""#,
11791 &store,
11792 );
11793 if let Err(ref e) = result {
11794 assert_ne!(e.code(), "CAL-E114", "empty scopes should skip enforcement");
11795 }
11796 }
11797
11798 #[test]
11799 fn test_scope_admin_bypasses_all() {
11800 let store = MockStore::empty();
11801 let config = CalExecutorConfig {
11802 caller_scopes: vec!["admin".to_string()],
11803 ..Default::default()
11804 };
11805 let ex = CalExecutor::new(config);
11806 let result = ex.execute(
11808 r#"ADD fact SET subject = "x" SET relation = "y" SET object = "z" REASON "test""#,
11809 &store,
11810 );
11811 if let Err(ref e) = result {
11812 assert_ne!(e.code(), "CAL-E114", "admin scope should bypass all checks");
11813 }
11814 }
11815
11816 #[test]
11823 fn test_assemble_post_merge_min_score_keeps_deterministic_grains() {
11824 let ex = exec();
11825 let store = MockStore::empty();
11826
11827 let semantic_low = CalGrainResult {
11828 hash: "11".repeat(32),
11829 grain_type: "fact".into(),
11830 score: 0.10,
11831 fields: serde_json::json!({"subject": "a", "relation": "r", "object": "o"}),
11832 score_breakdown: None,
11833 explanation: None,
11834 relative_time: None,
11835 is_deterministic: false,
11836 contested_by: None,
11837 };
11838 let semantic_high = CalGrainResult {
11839 hash: "22".repeat(32),
11840 grain_type: "fact".into(),
11841 score: 0.90,
11842 fields: serde_json::json!({"subject": "b", "relation": "r", "object": "o"}),
11843 score_breakdown: None,
11844 explanation: None,
11845 relative_time: None,
11846 is_deterministic: false,
11847 contested_by: None,
11848 };
11849 let deterministic = CalGrainResult {
11850 hash: "33".repeat(32),
11851 grain_type: "workflow".into(),
11852 score: 0.0, fields: serde_json::json!({"name": "boot", "status": "active"}),
11854 score_breakdown: None,
11855 explanation: None,
11856 relative_time: None,
11857 is_deterministic: true,
11858 contested_by: None,
11859 };
11860
11861 let payload = CalResultPayload::Assembled {
11862 grains: vec![
11863 semantic_low.clone(),
11864 semantic_high.clone(),
11865 deterministic.clone(),
11866 ],
11867 sources: vec![],
11868 total_tokens: 0,
11869 budget_limit: Some(4000),
11870 progressive: false,
11871 total_available: Some(3),
11872 };
11873
11874 let mut warnings = Vec::new();
11875 let out = ex
11876 .apply_assemble_post_merge_options(
11877 payload,
11878 &[WithOption::MinScore { score: 0.5 }],
11879 &mut warnings,
11880 &store,
11881 "test topic",
11882 )
11883 .unwrap();
11884
11885 match out {
11886 CalResultPayload::Assembled { grains, .. } => {
11887 let kept_hashes: Vec<&str> = grains.iter().map(|g| g.hash.as_str()).collect();
11888 assert!(
11889 kept_hashes.contains(&semantic_high.hash.as_str()),
11890 "high-scoring semantic grain must be retained, got: {:?}",
11891 kept_hashes
11892 );
11893 assert!(
11894 !kept_hashes.contains(&semantic_low.hash.as_str()),
11895 "low-scoring semantic grain must be dropped, got: {:?}",
11896 kept_hashes
11897 );
11898 assert!(
11899 kept_hashes.contains(&deterministic.hash.as_str()),
11900 "deterministic-source grain must NOT be dropped by min_score, got: {:?}",
11901 kept_hashes
11902 );
11903 }
11904 other => panic!("expected Assembled payload, got: {:?}", other),
11905 }
11906 }
11907
11908 #[test]
11910 fn test_execute_recall_marks_no_about_as_deterministic() {
11911 let store = MockStore::with_grains(vec![
11912 make_fact("alice", "knows", "bob"),
11913 make_fact("alice", "knows", "carol"),
11914 ]);
11915 let ex = exec();
11916
11917 let recall_stmt = RecallStmt {
11918 grain_type: GrainTypePlural::Facts,
11919 about: None, where_clause: Some(super::super::ast::WhereClause {
11921 condition: super::super::ast::Condition::Comparison {
11922 field: "subject".into(),
11923 comparator: super::super::ast::Comparator::Eq,
11924 value: super::super::ast::Value::String {
11925 value: "alice".into(),
11926 },
11927 span: None,
11928 },
11929 span: None,
11930 }),
11931 recent: None,
11932 since: None,
11933 until: None,
11934 like: None,
11935 between: None,
11936 contradictions: None,
11937 limit: None,
11938 as_format: None,
11939 span: None,
11940 };
11941
11942 let query = CalQuery {
11943 version: super::super::ast::CalVersion(1),
11944 statement: CalStatement::Recall(recall_stmt.clone()),
11945 pipeline: Vec::new(),
11946 with_options: Vec::new(),
11947 format: None,
11948 let_bindings: Vec::new(),
11949 let_values: Default::default(),
11950 user_vars: HashMap::new(),
11951 warnings: Vec::new(),
11952 };
11953
11954 let mut warnings = Vec::new();
11955 let payload = ex
11956 .execute_recall(&recall_stmt, &store, &query, &mut warnings)
11957 .unwrap();
11958
11959 match payload {
11960 CalResultPayload::Grains { grains, .. } => {
11961 assert!(!grains.is_empty(), "expected at least one grain");
11962 assert!(
11963 grains.iter().all(|g| g.is_deterministic),
11964 "every grain from a no-ABOUT RECALL must be flagged deterministic"
11965 );
11966 }
11967 other => panic!("expected Grains payload, got: {:?}", other),
11968 }
11969 }
11970
11971 #[test]
11973 fn test_execute_recall_with_about_not_deterministic() {
11974 let store = MockStore::with_grains(vec![make_fact("alice", "likes", "coffee")]);
11975 let ex = exec();
11976
11977 let recall_stmt = RecallStmt {
11978 grain_type: GrainTypePlural::Facts,
11979 about: Some(super::super::ast::AboutClause {
11980 text: "coffee preferences".into(),
11981 span: None,
11982 }),
11983 where_clause: None,
11984 recent: None,
11985 since: None,
11986 until: None,
11987 like: None,
11988 between: None,
11989 contradictions: None,
11990 limit: None,
11991 as_format: None,
11992 span: None,
11993 };
11994
11995 let query = CalQuery {
11996 version: super::super::ast::CalVersion(1),
11997 statement: CalStatement::Recall(recall_stmt.clone()),
11998 pipeline: Vec::new(),
11999 with_options: Vec::new(),
12000 format: None,
12001 let_bindings: Vec::new(),
12002 let_values: Default::default(),
12003 user_vars: HashMap::new(),
12004 warnings: Vec::new(),
12005 };
12006
12007 let mut warnings = Vec::new();
12008 let payload = ex
12009 .execute_recall(&recall_stmt, &store, &query, &mut warnings)
12010 .unwrap();
12011
12012 match payload {
12013 CalResultPayload::Grains { grains, .. } => {
12014 assert!(
12015 grains.iter().all(|g| !g.is_deterministic),
12016 "grains from an ABOUT RECALL must NOT be flagged deterministic"
12017 );
12018 }
12019 other => panic!("expected Grains payload, got: {:?}", other),
12020 }
12021 }
12022}