areev_cal/ast.rs
1//! CAL Abstract Syntax Tree types.
2//!
3//! All 12 statement variants are defined here, even though Phase 1 only
4//! executes RECALL and EXISTS. The parser needs to recognise every variant
5//! so it can produce meaningful error messages for unsupported statements.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use super::errors::Span;
12
13// ---------------------------------------------------------------------------
14// Version prefix
15// ---------------------------------------------------------------------------
16
17/// The `CAL/<n>` version prefix on a query (e.g. `CAL/1`).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub struct CalVersion(pub u32);
20
21impl Default for CalVersion {
22 fn default() -> Self {
23 Self(1)
24 }
25}
26
27// ---------------------------------------------------------------------------
28// Top-level query
29// ---------------------------------------------------------------------------
30
31/// A fully parsed CAL query — version prefix + statement + optional pipeline.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct CalQuery {
34 /// Explicit version prefix, if present. Defaults to `CAL/1`.
35 #[serde(default)]
36 pub version: CalVersion,
37
38 /// The core statement.
39 pub statement: CalStatement,
40
41 /// Pipeline stages applied after the statement (`| SELECT ...`, etc.).
42 pub pipeline: Vec<PipelineStage>,
43
44 /// `WITH` options (e.g. `WITH superseded`, `WITH score_breakdown`).
45 pub with_options: Vec<WithOption>,
46
47 /// `FORMAT` spec (e.g. `FORMAT json`, `FORMAT [markdown, json]`).
48 pub format: Option<FormatClause>,
49
50 /// `LET` bindings extracted before the statement (e.g.
51 /// `LET $x = SUBJECTS OF (...)`).
52 pub let_bindings: Vec<LetBinding>,
53
54 /// `WITH VARS { "key": "value", ... }` — user-injected display variables.
55 ///
56 /// These are string-only values accessible in FORMAT TEMPLATE via `{{$key}}`
57 /// syntax. They do NOT affect query execution — display only.
58 #[serde(default)]
59 pub user_vars: HashMap<String, String>,
60
61 /// `LET` bindings after evaluation, keyed by name without the `$`.
62 ///
63 /// Execution state, not query text, so it is never serialized: the parser
64 /// leaves this empty and the executor fills it once the bindings have run.
65 /// It exists because `$friends` in `WHERE subject IN $friends` has to
66 /// become a list of values somewhere, and threading a scope object through
67 /// every statement executor to do it would touch far more surface than
68 /// carrying the answer on the query does.
69 #[serde(skip)]
70 pub let_values: HashMap<String, Vec<String>>,
71
72 /// Warnings emitted during parsing (non-fatal).
73 #[serde(skip)]
74 pub warnings: Vec<super::errors::CalWarning>,
75}
76
77// ---------------------------------------------------------------------------
78// Statements (22 variants)
79// ---------------------------------------------------------------------------
80
81/// The 22 CAL statement types.
82///
83/// Phase 1 (Core conformance) executes `Recall` and `Exists`. All others
84/// parse correctly so the engine can report "unsupported in this tier" rather
85/// than a cryptic parse error.
86///
87/// Each variant carries serde aliases for its uppercase / PascalCase forms so
88/// JSON-CAL callers can use any casing for the `"kind"` tag.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "kind", rename_all = "snake_case")]
91#[allow(clippy::large_enum_variant)]
92pub enum CalStatement {
93 // ── Phase 1: Core (Conformance Level 1) ─────────────────────────────
94 /// `RECALL facts WHERE ...`
95 #[serde(alias = "RECALL", alias = "Recall")]
96 Recall(RecallStmt),
97
98 /// `RECALL ... INTERSECT RECALL ...`
99 #[serde(alias = "SET_OP", alias = "SetOp")]
100 SetOp(SetOpStmt),
101
102 /// `EXISTS facts WHERE ...`
103 #[serde(alias = "EXISTS", alias = "Exists")]
104 Exists(ExistsStmt),
105
106 /// `ASSEMBLE "topic" FROM ... WHERE ...`
107 #[serde(alias = "ASSEMBLE", alias = "Assemble")]
108 Assemble(AssembleStmt),
109
110 /// `HISTORY OF <hash>`
111 #[serde(alias = "HISTORY", alias = "History")]
112 History(HistoryStmt),
113
114 /// `EXPLAIN <query>`
115 #[serde(alias = "EXPLAIN", alias = "Explain")]
116 Explain(ExplainStmt),
117
118 /// `DESCRIBE facts` / `DESCRIBE SCHEMA`
119 #[serde(alias = "DESCRIBE", alias = "Describe")]
120 Describe(DescribeStmt),
121
122 /// `BATCH { ... ; ... }`
123 #[serde(alias = "BATCH", alias = "Batch")]
124 Batch(BatchStmt),
125
126 /// `COALESCE facts WHERE ...`
127 #[serde(alias = "COALESCE", alias = "Coalesce")]
128 Coalesce(CoalesceStmt),
129
130 // ── Tier 1: Write statements ────────────────────────────────────────
131 /// `ADD fact subject=... relation=... object=...`
132 #[serde(alias = "ADD", alias = "Add")]
133 Add(AddStmt),
134
135 /// `ADD workflow "name" [ON "trigger"] graph... [BIND ...] REASON "..."`
136 #[serde(alias = "ADD_WORKFLOW", alias = "AddWorkflow")]
137 AddWorkflow(AddWorkflowStmt),
138
139 /// `SUPERSEDE <hash> SET ... BECAUSE "..."`
140 #[serde(alias = "SUPERSEDE", alias = "Supersede")]
141 Supersede(SupersedeStmt),
142
143 /// `SUPERSEDE <hash> graph... [BIND ...] REASON "..."`
144 #[serde(alias = "SUPERSEDE_WORKFLOW", alias = "SupersedeWorkflow")]
145 SupersedeWorkflow(SupersedeWorkflowStmt),
146
147 /// `ACCUMULATE <grain_type> [<hash>] [WHERE ...] ADD ... [SET ...] REASON "..."`
148 #[serde(alias = "ACCUMULATE", alias = "Accumulate")]
149 Accumulate(AccumulateStmt),
150
151 /// `REVERT <hash> BECAUSE "..."`
152 #[serde(alias = "REVERT", alias = "Revert")]
153 Revert(RevertStmt),
154
155 /// `REPORT SUBJECT "<id>" [WITH text_mentions]` — the read-only DSAR
156 /// mirror of `FORGET SUBJECT` (OMS 1.6 draft).
157 ReportSubject(ReportSubjectStmt),
158
159 // ── Tier 2: Destructive statements (gated by allow_destructive_ops) ─
160 /// `FORGET <hash>` / `FORGET SUBJECT "<id>"`
161 Forget(ForgetStmt),
162
163 /// `PURGE OLDER THAN <n><d|h|m> [TYPE t] [IN "<namespace>"] [LIMIT <n>]`
164 Purge(PurgeStmt),
165
166 // ── Tier 3: Control (CAL 1.3 §8.15) ────────────────────────────────
167 /// `GRANT <verbs> ON <ns> TO "<principal>"`
168 #[serde(alias = "GRANT")]
169 Grant(GrantStmt),
170
171 /// `REVOKE <verbs> ON <ns> FROM "<principal>"`
172 #[serde(alias = "REVOKE")]
173 Revoke(RevokeStmt),
174
175 /// `SHOW GRANTS [FOR "<principal>"]`
176 #[serde(alias = "SHOW_GRANTS", alias = "ShowGrants")]
177 ShowGrants(ShowGrantsStmt),
178
179 // ── Tier 3: Governance (CAL 1.3 §8.16) ─────────────────────────────
180 /// `APPROVE <hash> BECAUSE "…"`
181 #[serde(alias = "APPROVE")]
182 Approve(GovernanceStmt),
183 /// `REJECT <hash> BECAUSE "…"`
184 #[serde(alias = "REJECT")]
185 Reject(GovernanceStmt),
186 /// `APPLY <hash> BECAUSE "…"`
187 #[serde(alias = "APPLY")]
188 ApplyRec(GovernanceStmt),
189 /// `ROLLBACK <hash> BECAUSE "…"`
190 #[serde(alias = "ROLLBACK")]
191 RollbackRec(GovernanceStmt),
192 /// `RUN LOOP [FULL] [WITH …]`
193 #[serde(alias = "RUN_LOOP", alias = "RunLoop")]
194 RunLoop(RunLoopStmt),
195
196 /// `REMEMBER "<content>" [WITH …]`
197 #[serde(alias = "REMEMBER")]
198 Remember(RememberStmt),
199
200 // ── Wave-2 reads (CAL 1.3) ─────────────────────────────────────────
201 /// `ENTITY "<s>" RELATION "<r>" AT <ms> [AXIS …]`
202 #[serde(alias = "ENTITY_AT", alias = "EntityAt")]
203 EntityAt(EntityAtStmt),
204 /// `RUN TRACE "<run-id>"`
205 #[serde(alias = "RUN_TRACE")]
206 RunTrace(RunTraceStmt),
207 /// `RUNS TOUCHING <hash>`
208 #[serde(alias = "RUNS_TOUCHING")]
209 RunsTouching(RunsTouchingStmt),
210 /// `DERIVED FROM <hash>`
211 #[serde(alias = "DERIVED_FROM")]
212 DerivedFrom(DerivedFromStmt),
213 /// `SHOW FORKS`
214 #[serde(alias = "SHOW_FORKS")]
215 ShowForks(ShowForksStmt),
216 /// `MERGE "<s>" RELATION "<r>" TO "<o>" BECAUSE "…"`
217 #[serde(alias = "MERGE")]
218 Merge(MergeStmt),
219 /// `RELATED "<start>" VIA "<relations>"`
220 #[serde(alias = "RELATED")]
221 Related(RelatedStmt),
222 /// `NOVELTY "<text>"`
223 #[serde(alias = "NOVELTY")]
224 Novelty(NoveltyStmt),
225
226 // ── Template management ──────────────────────────────────────────────
227 /// `DEFINE TEMPLATE "name" [DESCRIPTION "..."] [EXTENDS "parent"] [FOR facts, events] AS "source"`
228 DefineTemplate(DefineTemplateStmt),
229
230 /// `DROP TEMPLATE "name"`
231 DropTemplate(DropTemplateStmt),
232
233 // ── Saved query management ───────────────────────────────────────────
234 /// `DEFINE QUERY "name"($params) [DESCRIPTION "..."] AS { body }`
235 DefineQuery(DefineQueryStmt),
236
237 /// `DROP QUERY "name"`
238 DropQuery(DropQueryStmt),
239
240 /// `RUN "name"($param = value, ...) [WITH ...] [FORMAT ...]`
241 RunQuery(RunQueryStmt),
242}
243
244// ---------------------------------------------------------------------------
245// RECALL
246// ---------------------------------------------------------------------------
247
248/// `RECALL <grain_type_plural> [ABOUT "..."] [WHERE ...] [RECENT n] ...`
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct RecallStmt {
251 /// The grain type being queried (plural form in CAL syntax, e.g.
252 /// `facts`, `events`).
253 pub grain_type: GrainTypePlural,
254
255 /// Free-text `ABOUT "..."` clause for semantic search.
256 pub about: Option<AboutClause>,
257
258 /// Structured `WHERE ...` filter.
259 pub where_clause: Option<WhereClause>,
260
261 /// `RECENT <n>` shorthand for `ORDER BY created_at DESC LIMIT n`.
262 pub recent: Option<RecentClause>,
263
264 /// `SINCE "..."` temporal filter.
265 pub since: Option<SinceClause>,
266
267 /// `UNTIL "..."` temporal upper-bound. Can combine with SINCE for a range.
268 pub until: Option<UntilClause>,
269
270 /// `LIKE "..."` text-similarity filter.
271 pub like: Option<LikeClause>,
272
273 /// `BETWEEN "..." AND "..."` temporal range.
274 pub between: Option<BetweenClause>,
275
276 /// `CONTRADICTIONS OF (...)` sub-query.
277 pub contradictions: Option<ContradictionsClause>,
278
279 /// Inline `LIMIT` (separate from pipeline).
280 pub limit: Option<u64>,
281
282 // ── Phase 2 additions ────────────────────────────────────────────────
283 /// Per-query output format override (`AS json`, `AS [markdown, json]`, etc.).
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub as_format: Option<FormatClause>,
286
287 /// Source span of the entire statement.
288 #[serde(skip)]
289 pub span: Option<Span>,
290}
291
292// ---------------------------------------------------------------------------
293// SET operations
294// ---------------------------------------------------------------------------
295
296/// A set operation combining two or more queries.
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298pub struct SetOpStmt {
299 pub op: SetOp,
300 pub operands: Vec<CalStatement>,
301 #[serde(skip)]
302 pub span: Option<Span>,
303}
304
305/// Set operation type.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum SetOp {
309 Union,
310 Intersect,
311 Except,
312}
313
314// ---------------------------------------------------------------------------
315// EXISTS
316// ---------------------------------------------------------------------------
317
318/// `EXISTS <grain_type_plural> WHERE ...`
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct ExistsStmt {
321 pub grain_type: GrainTypePlural,
322 pub where_clause: Option<WhereClause>,
323 pub about: Option<AboutClause>,
324 #[serde(skip)]
325 pub span: Option<Span>,
326}
327
328// ---------------------------------------------------------------------------
329// ASSEMBLE
330// ---------------------------------------------------------------------------
331
332/// `ASSEMBLE "topic" FROM <source> [WHERE ...]`
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct AssembleStmt {
335 /// The topic or title of the assembly (Phase 1 field).
336 pub topic: String,
337 /// Source sub-query or grain set (Phase 1 single-source path).
338 pub from: Source,
339 pub where_clause: Option<WhereClause>,
340
341 // ── Phase 2 additions ────────────────────────────────────────────────
342 /// Explicit context name label (`ASSEMBLE "name"`). When present,
343 /// overrides `topic`.
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub context_name: Option<String>,
346
347 /// Multi-source FROM clause. When present, overrides the single `from`.
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub sources: Option<Vec<NamedSource>>,
350
351 /// `BUDGET <n>` clause — token budget for assembled output.
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub budget: Option<BudgetSpec>,
354
355 /// `PRIORITY label1: 0.7, label2: 0.3` — per-source priority weights.
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub priority: Option<Vec<PrioritySpec>>,
358
359 /// `FORMAT markdown` etc. — output format for assembled context.
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub format: Option<FormatClause>,
362
363 /// `FOR "someone"` — target audience / user for the assembly.
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub for_whom: Option<String>,
366
367 /// `WITH dedup(field), summarize` — assembly-specific WITH options.
368 #[serde(skip_serializing_if = "Vec::is_empty")]
369 pub assemble_with: Vec<AssembleWithOption>,
370
371 /// Recall-tuning options from the same `WITH dedup, <opts>` clause as
372 /// `assemble_with` — everything that isn't `dedup` (e.g. `query_expansion`,
373 /// `recency_weight`, `rerank`). Held on the ASSEMBLE itself, NOT routed to
374 /// the enclosing query, so they scope to this assemble's recall even when it
375 /// is nested (EXPLAIN / COALESCE / parens / brace / assemble-source).
376 #[serde(default, skip_serializing_if = "Vec::is_empty")]
377 pub with_options: Vec<WithOption>,
378
379 /// `STREAM ASSEMBLE ...` — enable SSE streaming (FR-004).
380 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
381 pub streaming: bool,
382
383 #[serde(skip)]
384 pub span: Option<Span>,
385}
386
387/// A labeled source in a multi-source `ASSEMBLE ... FROM label1: (RECALL ...),
388/// label2: (RECALL ...)` clause.
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct NamedSource {
391 /// The label for this source (e.g. `"recent"`, `"context"`).
392 pub label: String,
393 /// The sub-query producing this source's grains. Ignored when
394 /// [`literal`](Self::literal) is set (which parks a placeholder here so
395 /// the serialized AST shape stays unchanged for every existing consumer).
396 pub query: Box<CalStatement>,
397 /// `LITERAL "…"` — host-supplied text rendered at this source's authored
398 /// position instead of grains from a query.
399 ///
400 /// A production system prompt is grains INTERLEAVED with fixed text, and
401 /// some of that text is contractually mandatory. Without this the host has
402 /// to write the instruction into the memory as a grain first, which turns
403 /// a compliance-critical string into a mutable row; with it the string
404 /// lives in the statement, i.e. in code.
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub literal: Option<String>,
407 /// `PIN` — this source is non-degradable.
408 ///
409 /// The budget allocator satisfies pinned sources FIRST, at their full
410 /// cost, and the trim loop never truncates them; if the budget cannot hold
411 /// them the statement fails with `CAL-E122` rather than quietly
412 /// summarising the one section that had to survive verbatim. Orthogonal to
413 /// `PRIORITY`, which only weights how the REMAINING budget is shared.
414 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
415 pub pinned: bool,
416 /// Per-source WITH options (e.g. `WITH exhaustive`). When non-empty,
417 /// these override the parent query's with_options for this source.
418 #[serde(default, skip_serializing_if = "Vec::is_empty")]
419 pub with_options: Vec<WithOption>,
420 #[serde(skip)]
421 pub span: Option<Span>,
422}
423
424/// Unit for the BUDGET clause — `tokens` (default) or `grains`.
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "snake_case")]
427#[derive(Default)]
428pub enum BudgetUnit {
429 /// Token-based budget (default).
430 #[default]
431 Tokens,
432 /// Grain-count budget.
433 Grains,
434}
435
436/// `BUDGET <n> [tokens|grains]` — token/grain budget for assembled output.
437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
438pub struct BudgetSpec {
439 /// Number of tokens (or grains).
440 pub tokens: u32,
441 /// The unit for the budget limit.
442 #[serde(default)]
443 pub unit: BudgetUnit,
444 #[serde(skip)]
445 pub span: Option<Span>,
446}
447
448/// `PRIORITY label: weight` — priority weight for a named source.
449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
450pub struct PrioritySpec {
451 /// The source label this priority applies to.
452 pub label: String,
453 /// Weight (0.0..=1.0).
454 pub weight: f64,
455 #[serde(skip)]
456 pub span: Option<Span>,
457}
458
459/// ASSEMBLE-specific WITH options (distinct from the top-level WithOption).
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461#[serde(tag = "kind", rename_all = "snake_case")]
462pub enum AssembleWithOption {
463 /// Deduplicate near-identical entries, optionally by a specific field.
464 Dedup { field: Option<String> },
465}
466
467/// Source for ASSEMBLE FROM clause.
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469#[serde(tag = "kind", rename_all = "snake_case")]
470pub enum Source {
471 /// `FROM facts WHERE ...` — an inline query.
472 Query(Box<RecallStmt>),
473 /// `FROM $parameter` — a bound parameter holding a result set.
474 Parameter { name: String },
475 /// `FROM <hash>, <hash>, ...` — explicit hash list.
476 Hashes(Vec<String>),
477}
478
479// ---------------------------------------------------------------------------
480// HISTORY
481// ---------------------------------------------------------------------------
482
483/// `HISTORY OF <hash>` or `HISTORY WHERE subject = ... AND relation = ...`
484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
485pub struct HistoryStmt {
486 /// Content-address hash (Phase 1 path). Empty string when using WHERE.
487 pub hash: String,
488
489 // ── Phase 2 additions ────────────────────────────────────────────────
490 /// Structured WHERE clause for triple-based history lookup.
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub where_clause: Option<WhereClause>,
493
494 /// `DIFF sha256:bbb` — compare two versions.
495 #[serde(skip_serializing_if = "Option::is_none")]
496 pub diff_target: Option<String>,
497
498 #[serde(skip)]
499 pub span: Option<Span>,
500}
501
502// ---------------------------------------------------------------------------
503// EXPLAIN
504// ---------------------------------------------------------------------------
505
506/// `EXPLAIN <query>` — returns a query plan.
507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
508pub struct ExplainStmt {
509 pub inner: Box<CalStatement>,
510 #[serde(skip)]
511 pub span: Option<Span>,
512}
513
514// ---------------------------------------------------------------------------
515// DESCRIBE
516// ---------------------------------------------------------------------------
517
518/// `DESCRIBE facts` / `DESCRIBE SCHEMA` — introspection.
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520pub struct DescribeStmt {
521 pub target: DescribeTarget,
522 #[serde(skip)]
523 pub span: Option<Span>,
524}
525
526/// What to describe.
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528#[serde(rename_all = "snake_case")]
529pub enum DescribeTarget {
530 /// Describe the schema of a specific grain type.
531 GrainType(GrainTypePlural),
532 /// Describe the entire database schema.
533 Schema,
534
535 // ── Phase 2 additions ────────────────────────────────────────────────
536 /// `DESCRIBE CAPABILITIES` — CAL conformance level and supported features.
537 Capabilities,
538 /// `DESCRIBE SERVER` — server information (version, uptime, etc.).
539 Server,
540 /// `DESCRIBE FIELDS [grain_type]` — list filterable/sortable fields.
541 Fields(Option<GrainTypePlural>),
542 /// `DESCRIBE TEMPLATES` — list registered output templates.
543 Templates,
544 /// `DESCRIBE GRAMMAR` — dump the CAL grammar (BNF or similar).
545 Grammar,
546 /// `DESCRIBE QUERIES` — list registered saved queries.
547 Queries,
548 /// `DESCRIBE QUERY "name"` — details of a specific saved query.
549 Query(String),
550
551 // ── CAL 1.3 (Tier 3) ────────────────────────────────────────────────
552 /// `DESCRIBE PRINCIPAL "<name>"` — the principal's effective grants.
553 Principal(String),
554 /// `DESCRIBE LOOP` — loop health (last run, queue depth).
555 Loop,
556 /// `DESCRIBE ANALYZERS` — registered analyzers + effective config.
557 Analyzers,
558 /// `DESCRIBE OUTCOMES` — the Verify gate's measured outcomes.
559 Outcomes,
560 /// `DESCRIBE POLICY` — the effective host loop policy (read-only).
561 LoopPolicy,
562 /// `DESCRIBE STATS` — store counters.
563 Stats,
564 /// `DESCRIBE INTEGRITY` — integrity + content-address recheck.
565 Integrity,
566}
567
568// ---------------------------------------------------------------------------
569// BATCH
570// ---------------------------------------------------------------------------
571
572/// A single entry inside a BATCH block, carrying the statement together with
573/// any per-entry pipeline stages, FORMAT clause, WITH options, and user vars.
574#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
575pub struct BatchEntry {
576 pub statement: CalStatement,
577 #[serde(skip_serializing_if = "Vec::is_empty", default)]
578 pub pipeline: Vec<PipelineStage>,
579 #[serde(skip_serializing_if = "Vec::is_empty", default)]
580 pub with_options: Vec<super::ast::WithOption>,
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub format: Option<FormatClause>,
583 #[serde(skip_serializing_if = "HashMap::is_empty", default)]
584 pub user_vars: HashMap<String, String>,
585}
586
587/// `BATCH { stmt1 ; stmt2 ; ... }`
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589pub struct BatchStmt {
590 /// Positional (unlabeled) entries.
591 pub statements: Vec<BatchEntry>,
592
593 // ── Phase 2 additions ────────────────────────────────────────────────
594 /// Labeled entries: `BATCH { label1: RECALL ...; label2: RECALL ...; }`.
595 /// When present, results are keyed by label instead of index.
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub labeled: Option<Vec<(String, BatchEntry)>>,
598
599 #[serde(skip)]
600 pub span: Option<Span>,
601}
602
603// ---------------------------------------------------------------------------
604// COALESCE
605// ---------------------------------------------------------------------------
606
607/// `COALESCE <grain_type_plural> WHERE ...` (Phase 1) or
608/// `COALESCE { query1 } OR { query2 } ELSE { fallback }` (Phase 2).
609#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
610pub struct CoalesceStmt {
611 /// Grain type for Phase 1 single-branch path.
612 pub grain_type: GrainTypePlural,
613 /// WHERE clause for Phase 1 single-branch path.
614 pub where_clause: Option<WhereClause>,
615
616 // ── Phase 2 additions ────────────────────────────────────────────────
617 /// Multi-branch fallback chain: `{ query1 } OR { query2 } OR ...`.
618 /// Each branch is tried in order until one returns non-empty results.
619 #[serde(skip_serializing_if = "Vec::is_empty")]
620 pub branches: Vec<CoalesceBranch>,
621
622 /// Optional `ELSE { fallback }` — executed if all branches return empty.
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub else_branch: Option<Box<CalStatement>>,
625
626 #[serde(skip)]
627 pub span: Option<Span>,
628}
629
630/// A single branch in a `COALESCE { ... } OR { ... }` chain.
631#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
632pub struct CoalesceBranch {
633 /// The query to try for this branch.
634 pub query: CalStatement,
635 #[serde(skip)]
636 pub span: Option<Span>,
637}
638
639// ---------------------------------------------------------------------------
640// ADD (Tier 1)
641// ---------------------------------------------------------------------------
642
643/// `ADD <grain_type_singular> <field>=<value> ... [WITH ...]`
644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
645pub struct AddStmt {
646 pub grain_type: GrainTypeSingular,
647 /// Key-value pairs (`subject="john"`, `relation="likes"`, etc.).
648 pub fields: Vec<FieldAssignment>,
649 /// Mandatory REASON / BECAUSE clause (required for all Tier 1 writes).
650 pub reason: String,
651 /// Per-call intelligence options (`WITH extract_memories, auto_relate`, etc.).
652 #[serde(default)]
653 pub with_options: Vec<AddWithOption>,
654 #[serde(skip)]
655 pub span: Option<Span>,
656}
657
658/// A `field = value` assignment in an ADD or SET clause.
659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct FieldAssignment {
661 pub field: String,
662 pub value: Value,
663 #[serde(skip)]
664 pub span: Option<Span>,
665}
666
667// ---------------------------------------------------------------------------
668// ADD WORKFLOW (Tier 1 — graph syntax)
669// ---------------------------------------------------------------------------
670
671/// A graph edge in a workflow: `src -> dst [WHEN "cond"] [* N]`
672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
673pub struct GraphEdge {
674 pub src: String,
675 pub dst: String,
676 pub cond: Option<String>,
677 pub repeat: Option<u32>,
678}
679
680/// A BIND clause: `BIND node = sha256:hash`
681#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682pub struct BindClause {
683 pub node: String,
684 pub hash: String,
685}
686
687/// `ADD workflow "name" [ON "trigger"] graph... [BIND ...] REASON "..."`
688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
689pub struct AddWorkflowStmt {
690 /// Workflow name (positional string after `ADD workflow`).
691 pub name: String,
692 /// All nodes discovered during parsing (unique, in declaration order).
693 pub nodes: Vec<String>,
694 /// Graph edges parsed from arrow chains.
695 pub edges: Vec<GraphEdge>,
696 /// BIND clauses mapping nodes to Tool definition hashes.
697 pub bindings: Vec<BindClause>,
698 /// REASON / BECAUSE string.
699 pub reason: String,
700 /// Per-call intelligence options (`WITH ...`).
701 #[serde(default)]
702 pub with_options: Vec<AddWithOption>,
703 #[serde(skip)]
704 pub span: Option<Span>,
705}
706
707/// `SUPERSEDE <hash> [ON "trigger"] graph... [BIND ...] REASON "..."`
708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
709pub struct SupersedeWorkflowStmt {
710 /// Hash of the grain to supersede.
711 pub hash: String,
712 /// All nodes discovered during parsing.
713 pub nodes: Vec<String>,
714 /// Graph edges parsed from arrow chains.
715 pub edges: Vec<GraphEdge>,
716 /// BIND clauses.
717 pub bindings: Vec<BindClause>,
718 /// REASON / BECAUSE string.
719 pub reason: String,
720 #[serde(skip)]
721 pub span: Option<Span>,
722}
723
724// ---------------------------------------------------------------------------
725// SUPERSEDE (Tier 1)
726// ---------------------------------------------------------------------------
727
728/// `SUPERSEDE <hash> SET <field>=<value>, ... BECAUSE "reason"`
729#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
730pub struct SupersedeStmt {
731 pub hash: String,
732 pub set_clauses: Vec<FieldAssignment>,
733 pub reason: String,
734 #[serde(skip)]
735 pub span: Option<Span>,
736}
737
738// ---------------------------------------------------------------------------
739// ACCUMULATE (Tier 1)
740// ---------------------------------------------------------------------------
741
742/// `ACCUMULATE <grain_type> [<hash>] [WHERE ...] ADD ... [SET ...] REASON "..."`
743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
744pub struct AccumulateStmt {
745 /// Target grain type (singular: fact, event, state, etc.).
746 pub grain_type: GrainTypeSingular,
747 /// Resolution mode: either a content hash or a WHERE-based tip lookup.
748 pub target: AccumulateTarget,
749 /// Numeric delta operations (ADD field = value).
750 pub add_ops: Vec<DeltaOp>,
751 /// Last-writer-wins field replacements (SET field = value).
752 pub set_ops: Vec<FieldAssignment>,
753 /// Reason for the accumulation (required).
754 pub reason: String,
755 #[serde(skip)]
756 pub span: Option<Span>,
757}
758
759/// How to identify the grain to accumulate into.
760#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
761#[serde(tag = "kind", rename_all = "snake_case")]
762pub enum AccumulateTarget {
763 /// Resolve the current tip via entity_latest lookup.
764 TipResolved {
765 subject: String,
766 relation: String,
767 namespace: Option<String>,
768 },
769 /// Target a specific grain by hash (optimistic concurrency).
770 Hash { hash: String },
771}
772
773/// A numeric delta operation: `ADD field = value`.
774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
775pub struct DeltaOp {
776 pub field: String,
777 pub delta: f64,
778 #[serde(skip)]
779 pub span: Option<Span>,
780}
781
782// ---------------------------------------------------------------------------
783// REVERT (Tier 1)
784// ---------------------------------------------------------------------------
785
786/// `REVERT <hash> BECAUSE "reason"`
787#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
788pub struct RevertStmt {
789 pub hash: String,
790 pub reason: String,
791 #[serde(skip)]
792 pub span: Option<Span>,
793}
794
795// ---------------------------------------------------------------------------
796// FORGET (Tier 2)
797// ---------------------------------------------------------------------------
798
799/// `FORGET <hash> [BECAUSE "<why>"]` or
800/// `FORGET SUBJECT "<id>" [WITH text_mentions] BECAUSE "<why>"` (CAL 1.3
801/// §8.14 — the subject form parses into [`ForgetTarget::User`], the store's
802/// identity erasure).
803///
804/// Capped by `CalExecutorConfig::allow_destructive_ops`; authorized by the
805/// session's `delete` (hash) / `erase` (subject) grant. `Scope` stays
806/// unreachable from text. BECAUSE is optional-but-recorded on the hash form
807/// (it predates the requirement) and mandatory on the subject form.
808#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
809pub struct ForgetStmt {
810 /// What to forget (hash, user, or scope).
811 pub target: ForgetTarget,
812 /// The recorded reason (BECAUSE). Mandatory for non-hash targets.
813 #[serde(default, skip_serializing_if = "Option::is_none")]
814 pub reason: Option<String>,
815 /// `WITH text_mentions` — extend a subject erasure to grains whose
816 /// indexed text mentions the identity (search symmetry; subject form
817 /// only).
818 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
819 pub text_mentions: bool,
820 #[serde(skip)]
821 pub span: Option<Span>,
822}
823
824/// Target of a FORGET statement.
825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
826#[serde(tag = "kind", rename_all = "snake_case")]
827pub enum ForgetTarget {
828 /// `FORGET <hash>` — remove a single grain by content-address hash.
829 Hash { hash: String },
830 /// `FORGET USER "<user_id>"` — crypto-erase all data for a user.
831 User { user_id: String },
832 /// `FORGET SCOPE "<scope>"` — crypto-erase all data in a scope.
833 Scope { scope: String },
834}
835
836// ---------------------------------------------------------------------------
837// PURGE (Tier 2)
838// ---------------------------------------------------------------------------
839
840/// `PURGE OLDER THAN <n><d|h|m> [TYPE <grain-type>] [IN "<namespace>"]
841/// BECAUSE "<why>"` — the retention sweep (CAL 1.3 §8.14).
842///
843/// Capped by `CalExecutorConfig::allow_destructive_ops`; authorized by the
844/// session's `erase` grant on the swept namespace. BECAUSE is mandatory.
845#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
846pub struct PurgeStmt {
847 /// Minimum age in days (from `OLDER THAN <n><unit>`; h/m convert).
848 pub min_age_days: Option<f64>,
849 /// Namespace scope (from `IN "<namespace>"`). Default: the session
850 /// namespace, then "shared" — never an implicit all-namespace sweep.
851 pub namespace: Option<String>,
852 /// Maximum grains to purge (from `LIMIT <n>`). Default: 1000.
853 pub limit: Option<usize>,
854 /// `TYPE <t>` — restrict the sweep to one grain type (e.g. `event`).
855 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub grain_type: Option<String>,
857 /// The recorded reason (BECAUSE). Mandatory from text.
858 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub reason: Option<String>,
860 #[serde(skip)]
861 pub span: Option<Span>,
862}
863
864// ---------------------------------------------------------------------------
865// REPORT SUBJECT (read-only DSAR)
866// ---------------------------------------------------------------------------
867
868/// `REPORT SUBJECT "<id>" [WITH text_mentions]` — the read-only DSAR
869/// selection (OMS 1.6 draft): everything `FORGET SUBJECT` would erase for
870/// one identity — exact + partition keys, full history — hydrated instead
871/// of erased (GDPR Art. 15 access / Art. 20 portability). A pure read:
872/// classifies `Read`, needs the `read` grant only, and is available on the
873/// token-less read-only console.
874#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
875pub struct ReportSubjectStmt {
876 /// The identity to report on.
877 pub subject_id: String,
878 /// `WITH text_mentions` — extend the selection to grains whose indexed
879 /// text mentions the identity (search symmetry with the erasure form).
880 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
881 pub text_mentions: bool,
882 #[serde(skip)]
883 pub span: Option<Span>,
884}
885
886// ---------------------------------------------------------------------------
887// Wave-2 reads (CAL 1.3): as-of, the run↔memory join, reverse provenance
888// ---------------------------------------------------------------------------
889
890/// `ENTITY "<subject>" RELATION "<relation>" AT <epoch-ms>
891/// [AXIS world|knowledge]` — the bitemporal as-of read: what was true in
892/// the world at T (`world`, validity windows) or what the agent knew at T
893/// (`knowledge`, the supersession chain).
894#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
895pub struct EntityAtStmt {
896 pub subject: String,
897 pub relation: String,
898 pub at_ms: i64,
899 /// `world` (default) | `knowledge`.
900 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub axis: Option<String>,
902 #[serde(skip)]
903 pub span: Option<Span>,
904}
905
906/// `RUN TRACE "<run-id>" [LIMIT <n>]` — everything a run recorded, plus
907/// what it produced downstream (the join between execution history and
908/// semantic memory, in one statement).
909#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
910pub struct RunTraceStmt {
911 pub run_id: String,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub limit: Option<usize>,
914 #[serde(skip)]
915 pub span: Option<Span>,
916}
917
918/// `RUNS TOUCHING <hash> [DEPTH <n>]` — which runs produced or refined a
919/// grain (walks provenance both ways; reads leave no grain and are not
920/// recorded).
921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
922pub struct RunsTouchingStmt {
923 pub hash: String,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub depth: Option<usize>,
926 #[serde(skip)]
927 pub span: Option<Span>,
928}
929
930/// `DERIVED FROM <hash>` — reverse provenance: the grains distilled from
931/// a source.
932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
933pub struct DerivedFromStmt {
934 pub hash: String,
935 #[serde(skip)]
936 pub span: Option<Span>,
937}
938
939/// `MERGE "<subject>" RELATION "<relation>" TO "<object>"
940/// [CONFIDENCE <n>] BECAUSE "<why>"` — close an open fork: a resolved
941/// value that supersedes every live tip (the merge grain records all
942/// parents). Requires the `supersede` grant; refuses when no fork is
943/// open (a merge of one head would be a plain supersede).
944#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
945pub struct MergeStmt {
946 pub subject: String,
947 pub relation: String,
948 pub object: String,
949 #[serde(default, skip_serializing_if = "Option::is_none")]
950 pub confidence: Option<f64>,
951 pub reason: String,
952 #[serde(skip)]
953 pub span: Option<Span>,
954}
955
956/// `RELATED "<start>" VIA "<r1,r2>" [DIRECTION out|in|both] [DEPTH <n>]
957/// [LIMIT <n>]` — the bounded k-hop entity walk. `in`/`both` only see
958/// relations the file declares entity-valued.
959#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
960pub struct RelatedStmt {
961 pub start: String,
962 /// Comma-separated relation list, as written.
963 pub relations: String,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
965 pub direction: Option<String>,
966 #[serde(default, skip_serializing_if = "Option::is_none")]
967 pub depth: Option<usize>,
968 #[serde(default, skip_serializing_if = "Option::is_none")]
969 pub limit: Option<usize>,
970 #[serde(skip)]
971 pub span: Option<Span>,
972}
973
974/// `NOVELTY "<text>" [SUBJECT "<s>"] [RELATION "<r>"] [LIMIT <k>]` — the
975/// paraphrase check: nearest existing grains to a candidate text.
976/// Requires a host-installed embedder; a clean refusal otherwise.
977#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
978pub struct NoveltyStmt {
979 pub text: String,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub subject: Option<String>,
982 #[serde(default, skip_serializing_if = "Option::is_none")]
983 pub relation: Option<String>,
984 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub limit: Option<usize>,
986 #[serde(skip)]
987 pub span: Option<Span>,
988}
989
990/// `SHOW FORKS` — the open forks (subject+relation pairs with >1 live
991/// head), first-class.
992#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
993pub struct ShowForksStmt {
994 #[serde(skip)]
995 pub span: Option<Span>,
996}
997
998// ---------------------------------------------------------------------------
999// REMEMBER (Tier 1 — CAL 1.3)
1000// ---------------------------------------------------------------------------
1001
1002/// `REMEMBER "<content>" [WITH session("<id>"), role("<r>"), run("<id>")]`
1003///
1004/// Capture free text as an Event grain — the onboarding verb, in the
1005/// language. The observer is the bound session's principal (never
1006/// statement text); LLM fact extraction stays a host concern
1007/// (`areev remember --model …`), so the statement carries no model names.
1008#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1009pub struct RememberStmt {
1010 pub content: String,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1012 pub session_id: Option<String>,
1013 /// `user` | `assistant` | `system` | `tool`.
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub role: Option<String>,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub run_id: Option<String>,
1018 #[serde(skip)]
1019 pub span: Option<Span>,
1020}
1021
1022// ---------------------------------------------------------------------------
1023// Governance (Tier 3 — CAL 1.3 §8.16)
1024// ---------------------------------------------------------------------------
1025
1026/// `APPROVE <hash> BECAUSE "…"` / `REJECT …` / `APPLY …` / `ROLLBACK …`.
1027///
1028/// The loop lifecycle in the language. BECAUSE is mandatory — a parse
1029/// error without, matching the engine's own non-empty check (two layers).
1030/// The actor, scopes, and observer come from the bound session, never from
1031/// the statement; the four gates (separation of duties, self-approval
1032/// block, two-key destructive apply, hash-chained audit) are enforced by
1033/// the engine exactly as on every other surface.
1034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1035pub struct GovernanceStmt {
1036 /// The recommendation's content address.
1037 pub hash: String,
1038 /// The mandatory written reason.
1039 pub reason: String,
1040 #[serde(skip)]
1041 pub span: Option<Span>,
1042}
1043
1044/// `RUN LOOP [FULL] [WITH min_new(N), if_stale("6h")]` — trigger the
1045/// analysis pass. Carries no credentials and no model names: LLM backends
1046/// are host configuration, never statement text.
1047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1048pub struct RunLoopStmt {
1049 /// `FULL` — the whole-memory reflect sweep.
1050 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1051 pub full_sweep: bool,
1052 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub min_new: Option<u64>,
1054 /// `if_stale` duration, milliseconds.
1055 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub if_stale_ms: Option<i64>,
1057 #[serde(skip)]
1058 pub span: Option<Span>,
1059}
1060
1061// ---------------------------------------------------------------------------
1062// GRANT / REVOKE / SHOW GRANTS (Tier 3 — CAL 1.3 §8.15)
1063// ---------------------------------------------------------------------------
1064
1065/// `GRANT <verb>[, <verb>…] ON <ns|*> TO "<principal>" [WITH because("…")]`
1066///
1067/// Writes a grant grain (`Fact + mg:permits` in the reserved `agent:authz`
1068/// namespace). Append-only — Tier 3 is gated by the `admin` verb and capped
1069/// by `tier1_enabled`, untouched by `allow_destructive_ops`. Verbs are
1070/// validated against the verb registry at execution.
1071#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1072pub struct GrantStmt {
1073 pub verbs: Vec<String>,
1074 /// Governed namespaces (`*` = every namespace).
1075 pub namespaces: Vec<String>,
1076 pub principal: String,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub reason: Option<String>,
1079 #[serde(skip)]
1080 pub span: Option<Span>,
1081}
1082
1083/// `REVOKE <verb>[, <verb>…] ON <ns|*> FROM "<principal>" [WITH because("…")]`
1084///
1085/// Retraction by supersession: each covering grant grain is superseded with
1086/// the reduced grant (or a retraction record when nothing remains) — nothing
1087/// is deleted; grant history stays append-only.
1088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089pub struct RevokeStmt {
1090 pub verbs: Vec<String>,
1091 pub namespaces: Vec<String>,
1092 pub principal: String,
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1094 pub reason: Option<String>,
1095 #[serde(skip)]
1096 pub span: Option<Span>,
1097}
1098
1099/// `SHOW GRANTS [FOR "<principal>"]` — the live grants, one row per grant
1100/// grain. Sugar over recalling the `PERMISSION` relation in `agent:authz`.
1101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1102pub struct ShowGrantsStmt {
1103 #[serde(default, skip_serializing_if = "Option::is_none")]
1104 pub principal: Option<String>,
1105 #[serde(skip)]
1106 pub span: Option<Span>,
1107}
1108
1109// ---------------------------------------------------------------------------
1110// DEFINE TEMPLATE / DROP TEMPLATE
1111// ---------------------------------------------------------------------------
1112
1113/// `DEFINE TEMPLATE "name" [DESCRIPTION "..."] [EXTENDS "parent"] [FOR facts, events] AS "source"`
1114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1115pub struct DefineTemplateStmt {
1116 /// Template name (validated: `^[a-zA-Z][a-zA-Z0-9 _-]{0,63}$`).
1117 pub name: String,
1118 /// Optional human-readable description.
1119 #[serde(skip_serializing_if = "Option::is_none")]
1120 pub description: Option<String>,
1121 /// Optional parent template name (1-level inheritance).
1122 #[serde(skip_serializing_if = "Option::is_none")]
1123 pub parent: Option<String>,
1124 /// Optional grain type restriction (e.g. `FOR facts, events`).
1125 #[serde(skip_serializing_if = "Vec::is_empty")]
1126 pub grain_types: Vec<String>,
1127 /// Template source (Mustache-subset).
1128 ///
1129 /// For a sectioned body this holds [`TemplateSectionSources::to_source`],
1130 /// so everything downstream that persists or displays a template keeps
1131 /// working on one canonical text form.
1132 pub source: String,
1133 /// Sectioned body (OMS CAL §10.6). `None` for the `AS "<text>"` form.
1134 #[serde(default, skip_serializing_if = "Option::is_none")]
1135 pub sections: Option<TemplateSectionSources>,
1136 #[serde(skip)]
1137 pub span: Option<Span>,
1138}
1139
1140/// Raw section bodies of a §10.6 sectioned template.
1141///
1142/// Held as unparsed text: the AST is syntax and the template engine owns
1143/// parsing, which also keeps the JSON-CAL wire form and the persisted form
1144/// the same shape. `None` means the section was not written and is inherited
1145/// from the parent (§10.7) — distinct from `Some("")`, which deliberately
1146/// overrides the parent with nothing.
1147#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
1148pub struct TemplateSectionSources {
1149 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub header: Option<String>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1152 pub element: Option<String>,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub element_summary: Option<String>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub element_omit: Option<String>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub source_break: Option<String>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub footer: Option<String>,
1161}
1162
1163impl TemplateSectionSources {
1164 /// True when no section was written.
1165 pub fn is_empty(&self) -> bool {
1166 self.header.is_none()
1167 && self.element.is_none()
1168 && self.element_summary.is_none()
1169 && self.element_omit.is_none()
1170 && self.source_break.is_none()
1171 && self.footer.is_none()
1172 }
1173
1174 /// Render back to canonical CAL section text.
1175 ///
1176 /// Round-trips through the lexer: it strips exactly one newline of layout
1177 /// at each end of a body, which is what this emits.
1178 pub fn to_source(&self) -> String {
1179 let mut out = String::new();
1180 for (kw, body) in [
1181 ("HEADER", &self.header),
1182 ("ELEMENT", &self.element),
1183 ("ELEMENT_SUMMARY", &self.element_summary),
1184 ("ELEMENT_OMIT", &self.element_omit),
1185 ("SOURCE_BREAK", &self.source_break),
1186 ("FOOTER", &self.footer),
1187 ] {
1188 if let Some(body) = body {
1189 out.push_str(kw);
1190 out.push_str(" {\n");
1191 out.push_str(body);
1192 out.push_str("\n}\n");
1193 }
1194 }
1195 out
1196 }
1197}
1198
1199/// `DROP TEMPLATE "name"`
1200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1201pub struct DropTemplateStmt {
1202 /// Template name to drop.
1203 pub name: String,
1204 #[serde(skip)]
1205 pub span: Option<Span>,
1206}
1207
1208// ---------------------------------------------------------------------------
1209// DEFINE QUERY / DROP QUERY / RUN
1210// ---------------------------------------------------------------------------
1211
1212/// A parameter declaration in a saved query definition.
1213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1214pub struct QueryParam {
1215 /// Parameter name (without the `$` prefix).
1216 pub name: String,
1217 /// Optional default value.
1218 #[serde(skip_serializing_if = "Option::is_none")]
1219 pub default: Option<Value>,
1220}
1221
1222/// `DEFINE QUERY "name"($params) [DESCRIPTION "..."] AS { body }`
1223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1224pub struct DefineQueryStmt {
1225 /// Query name (validated: `^[a-zA-Z][a-zA-Z0-9 _-]{0,63}$`).
1226 pub name: String,
1227 /// Optional human-readable description.
1228 #[serde(skip_serializing_if = "Option::is_none")]
1229 pub description: Option<String>,
1230 /// Parameter declarations with optional defaults.
1231 #[serde(skip_serializing_if = "Vec::is_empty")]
1232 pub params: Vec<QueryParam>,
1233 /// Raw CAL body text (stored as-is, parsed at RUN time).
1234 pub body: String,
1235 #[serde(skip)]
1236 pub span: Option<Span>,
1237}
1238
1239/// `DROP QUERY "name"`
1240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1241pub struct DropQueryStmt {
1242 /// Query name to drop.
1243 pub name: String,
1244 #[serde(skip)]
1245 pub span: Option<Span>,
1246}
1247
1248/// `RUN "name"($param = value, ...) [WITH ...] [FORMAT ...]`
1249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1250pub struct RunQueryStmt {
1251 /// Saved query name to execute.
1252 pub name: String,
1253 /// Parameter bindings ($name = value).
1254 #[serde(skip_serializing_if = "Vec::is_empty")]
1255 pub bindings: Vec<(String, Value)>,
1256 #[serde(skip)]
1257 pub span: Option<Span>,
1258}
1259
1260// ---------------------------------------------------------------------------
1261// WHERE clause
1262// ---------------------------------------------------------------------------
1263
1264/// Structured filter clause.
1265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1266pub struct WhereClause {
1267 pub condition: Condition,
1268 #[serde(skip)]
1269 pub span: Option<Span>,
1270}
1271
1272/// A filter condition (possibly nested).
1273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1274#[serde(tag = "kind", rename_all = "snake_case")]
1275pub enum Condition {
1276 /// `field <op> value`
1277 Comparison {
1278 field: String,
1279 comparator: Comparator,
1280 value: Value,
1281 #[serde(skip)]
1282 span: Option<Span>,
1283 },
1284
1285 /// `field IN (v1, v2, ...)`
1286 In {
1287 field: String,
1288 values: Vec<Value>,
1289 #[serde(skip)]
1290 span: Option<Span>,
1291 },
1292
1293 /// `field NOT IN (v1, v2, ...)`
1294 NotIn {
1295 field: String,
1296 values: Vec<Value>,
1297 #[serde(skip)]
1298 span: Option<Span>,
1299 },
1300
1301 /// `field IS NULL`
1302 IsNull {
1303 field: String,
1304 #[serde(skip)]
1305 span: Option<Span>,
1306 },
1307
1308 /// `field IS NOT NULL`
1309 IsNotNull {
1310 field: String,
1311 #[serde(skip)]
1312 span: Option<Span>,
1313 },
1314
1315 /// `field CONTAINS "text"`
1316 Contains {
1317 field: String,
1318 value: String,
1319 #[serde(skip)]
1320 span: Option<Span>,
1321 },
1322
1323 /// `field STARTS WITH "text"`
1324 StartsWith {
1325 field: String,
1326 value: String,
1327 #[serde(skip)]
1328 span: Option<Span>,
1329 },
1330
1331 /// `cond AND cond`
1332 And {
1333 left: Box<Condition>,
1334 right: Box<Condition>,
1335 #[serde(skip)]
1336 span: Option<Span>,
1337 },
1338
1339 /// `cond OR cond`
1340 Or {
1341 left: Box<Condition>,
1342 right: Box<Condition>,
1343 #[serde(skip)]
1344 span: Option<Span>,
1345 },
1346
1347 /// `NOT cond`
1348 Not {
1349 inner: Box<Condition>,
1350 #[serde(skip)]
1351 span: Option<Span>,
1352 },
1353
1354 // ── Phase 2 additions ────────────────────────────────────────────────
1355 /// `field IS PREFERENCE` / `field IS KNOWLEDGE` etc. — category filter.
1356 IsCategory {
1357 field: String,
1358 category: String,
1359 #[serde(skip)]
1360 span: Option<Span>,
1361 },
1362}
1363
1364// ---------------------------------------------------------------------------
1365// Comparators
1366// ---------------------------------------------------------------------------
1367
1368/// Comparison operators.
1369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1370#[serde(rename_all = "snake_case")]
1371pub enum Comparator {
1372 /// `=`
1373 Eq,
1374 /// `!=`
1375 NotEq,
1376 /// `>=`
1377 Gte,
1378 /// `<=`
1379 Lte,
1380 /// `>`
1381 Gt,
1382 /// `<`
1383 Lt,
1384}
1385
1386impl std::fmt::Display for Comparator {
1387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1388 match self {
1389 Self::Eq => write!(f, "="),
1390 Self::NotEq => write!(f, "!="),
1391 Self::Gte => write!(f, ">="),
1392 Self::Lte => write!(f, "<="),
1393 Self::Gt => write!(f, ">"),
1394 Self::Lt => write!(f, "<"),
1395 }
1396 }
1397}
1398
1399// ---------------------------------------------------------------------------
1400// Values
1401// ---------------------------------------------------------------------------
1402
1403/// A literal value or parameter reference in a CAL expression.
1404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1405#[serde(tag = "kind", rename_all = "snake_case")]
1406pub enum Value {
1407 /// A quoted string literal: `"hello"`.
1408 String { value: String },
1409 /// A numeric literal: `42`, `3.14`.
1410 Number { value: f64 },
1411 /// A boolean literal: `true` / `false`.
1412 Boolean { value: bool },
1413 /// An array literal: `["a", "b"]`.
1414 Array { values: Vec<Value> },
1415 /// A content-address hash literal: `#abcdef01...`.
1416 Hash { value: String },
1417 /// A bound parameter reference: `$name`.
1418 Parameter { name: String },
1419}
1420
1421impl Value {
1422 /// Human-readable type label for error messages. Stable, does not
1423 /// expose the underlying struct shape.
1424 pub fn type_name(&self) -> &'static str {
1425 match self {
1426 Self::String { .. } => "string",
1427 Self::Number { .. } => "number",
1428 Self::Boolean { .. } => "boolean",
1429 Self::Array { .. } => "array",
1430 Self::Hash { .. } => "hash",
1431 Self::Parameter { .. } => "parameter",
1432 }
1433 }
1434}
1435
1436impl std::fmt::Display for Value {
1437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1438 match self {
1439 Self::String { value } => write!(f, "\"{}\"", value),
1440 Self::Number { value } => write!(f, "{}", value),
1441 Self::Boolean { value } => write!(f, "{}", value),
1442 Self::Array { values } => {
1443 write!(f, "[")?;
1444 for (i, v) in values.iter().enumerate() {
1445 if i > 0 {
1446 write!(f, ", ")?;
1447 }
1448 write!(f, "{}", v)?;
1449 }
1450 write!(f, "]")
1451 }
1452 Self::Hash { value } => write!(f, "#{}", value),
1453 Self::Parameter { name } => write!(f, "${}", name),
1454 }
1455 }
1456}
1457
1458// ---------------------------------------------------------------------------
1459// Pipeline stages
1460// ---------------------------------------------------------------------------
1461
1462/// A pipeline stage following `|`.
1463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1464#[serde(tag = "stage", rename_all = "snake_case")]
1465pub enum PipelineStage {
1466 /// `| SELECT field1, field2, ...`
1467 Select {
1468 fields: Vec<String>,
1469 #[serde(skip)]
1470 span: Option<Span>,
1471 },
1472
1473 /// `| ORDER BY field [ASC|DESC]`
1474 OrderBy {
1475 field: String,
1476 descending: bool,
1477 #[serde(skip)]
1478 span: Option<Span>,
1479 },
1480
1481 /// `| LIMIT n`
1482 Limit {
1483 value: u64,
1484 #[serde(skip)]
1485 span: Option<Span>,
1486 },
1487
1488 /// `| OFFSET n`
1489 Offset {
1490 value: u64,
1491 #[serde(skip)]
1492 span: Option<Span>,
1493 },
1494
1495 /// `| COUNT`
1496 Count {
1497 #[serde(skip)]
1498 span: Option<Span>,
1499 },
1500
1501 /// `| FIRST`
1502 First {
1503 #[serde(skip)]
1504 span: Option<Span>,
1505 },
1506
1507 /// `| SUBJECTS` — extract the `subject` field from each Fact.
1508 Subjects {
1509 #[serde(skip)]
1510 span: Option<Span>,
1511 },
1512
1513 /// `| OBJECTS` — extract the `object` field from each Fact.
1514 Objects {
1515 #[serde(skip)]
1516 span: Option<Span>,
1517 },
1518
1519 /// `| HASHES` — extract the content-address hash of each grain.
1520 Hashes {
1521 #[serde(skip)]
1522 span: Option<Span>,
1523 },
1524
1525 /// `| GROUP BY field`
1526 GroupBy {
1527 field: String,
1528 #[serde(skip)]
1529 span: Option<Span>,
1530 },
1531
1532 /// `| PROJECT field1, field2, ...` (alias for SELECT with remapping).
1533 Project {
1534 fields: Vec<ProjectField>,
1535 #[serde(skip)]
1536 span: Option<Span>,
1537 },
1538
1539 /// `WHERE condition` appearing after pipeline stages (post-pipeline filter).
1540 ///
1541 /// Allows queries like `RECALL facts SELECT subject WHERE subject = "john"`
1542 /// where WHERE follows a pipeline stage rather than appearing in the RECALL
1543 /// statement body.
1544 Filter {
1545 condition: Condition,
1546 #[serde(skip)]
1547 span: Option<Span>,
1548 },
1549}
1550
1551/// A field in a `PROJECT` clause, optionally with an alias.
1552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1553pub struct ProjectField {
1554 pub field: String,
1555 pub alias: Option<String>,
1556}
1557
1558// ---------------------------------------------------------------------------
1559// WITH options
1560// ---------------------------------------------------------------------------
1561
1562/// A `WITH` option modifying query behaviour.
1563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1564#[serde(tag = "option", rename_all = "snake_case")]
1565pub enum WithOption {
1566 /// Include superseded (historical) grains in results.
1567 Superseded,
1568 /// Include relevance score breakdown per result.
1569 ScoreBreakdown,
1570 /// Include a human-readable explanation of ranking.
1571 Explanation,
1572 /// Include provenance chain in results.
1573 Provenance,
1574 /// Enable contradiction detection on the result set.
1575 ContradictionDetection,
1576 /// Apply MMR diversity to the result set.
1577 Diversity { lambda: Option<f64> },
1578 /// Deduplicate results, optionally keyed by a specific field name.
1579 /// EBNF: `"dedup" , "(" , field_name , ")"`.
1580 Dedup { field: Option<String> },
1581
1582 /// Progressive disclosure level (OMS §4 `progressive_disclosure(level)`).
1583 /// `level` is `summary | headlines | full`. Bare form (no parens) maps
1584 /// to `None` and lets the assembler pick a default.
1585 ProgressiveDisclosure { level: Option<String> },
1586
1587 /// Consistency level (OMS §4 `consistency(level)` where
1588 /// `level = "eventual" | "bounded" | "linearizable"`).
1589 Consistency { level: Option<String> },
1590
1591 /// Locale hint for humanization / collation (OMS §4 `locale("en-US")`).
1592 Locale { tag: String },
1593
1594 /// Cache directive (OMS §4 `cache(ttl=300)`).
1595 Cache { ttl_seconds: u64 },
1596
1597 // -- Recall feature flags (parity with HTTP/gRPC/MCP/A2A) ---------------
1598 /// Enable cross-encoder reranking (requires `rerank` feature).
1599 /// Optional model name selects a specific reranker from the registry.
1600 Rerank { model: Option<String> },
1601 /// Enable LLM listwise reranking (requires `llm-rerank` feature).
1602 /// Optional model name selects a specific LLM reranker from the registry.
1603 LlmRerank { model: Option<String> },
1604 /// Enable rule-based query expansion (stemming + synonyms).
1605 QueryExpansion,
1606 /// ADR-023: Enable rule-based query decomposition (2-4 sub-queries per strategy).
1607 QueryDecompose,
1608 /// Enable hypothetical document embeddings.
1609 Hyde,
1610 /// Keep only newest grain per (subject, relation).
1611 ConflictResolution,
1612 /// Include `derived_from` source grains in results.
1613 IncludeSources,
1614 /// Annotate results with relative time labels (e.g. "2 weeks ago").
1615 AnnotateRelativeTime,
1616 /// Set recency weight for temporal freshness scoring.
1617 RecencyWeight { weight: f64 },
1618 /// Set minimum relevance score threshold.
1619 MinScore { score: f64 },
1620 /// Enable entity-graph multi-hop retrieval (1-3 hops).
1621 MultiHop { hops: u64 },
1622 /// Set session affinity boost factor [0.0–1.0].
1623 SessionAffinity { boost: f64 },
1624 /// Set subject affinity boost factor [0.0–1.0].
1625 SubjectAffinity { boost: f64 },
1626 /// FR-005: Minimum grains per namespace for cross-session coverage.
1627 SessionCoverage { min_per_ns: u64 },
1628 /// FR-005: Maximum unique namespaces in results.
1629 MaxNamespaces { max: u64 },
1630 /// WI-EXHAUST: Enable exhaustive entity-class recall.
1631 /// Optional rounds parameter.
1632 Exhaustive { max_rounds: Option<u64> },
1633 /// RF-3: Session-census retrieval.
1634 /// Optional parameters: min_per_session, min_score.
1635 SessionCensus {
1636 min_per_session: Option<u64>,
1637 min_score: Option<f64>,
1638 },
1639 /// RQ-3: Keep superseded grains at natural scores for aggregation queries.
1640 AggregationIntent,
1641 /// Enrich preference queries with co-occurring session grains.
1642 PreferenceEnrichment,
1643}
1644
1645/// A `WITH` option on an `ADD` statement controlling intelligence behaviour.
1646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1647#[serde(tag = "option", rename_all = "snake_case")]
1648pub enum AddWithOption {
1649 /// Extract temporal references from content → auto-populate `valid_from`.
1650 ExtractEventDate,
1651 /// Auto-detect updates/extends relationships with existing grains.
1652 AutoRelate,
1653 /// Decompose content into atomic facts linked via `derived_from`.
1654 ExtractMemories,
1655 /// Force immediate commit (bypass write batch buffer).
1656 Sync,
1657 /// `WITH occurrence` (ADD tool only): stamp a synthetic per-call
1658 /// identity so byte-identical retries stay distinct occurrences
1659 /// instead of collapsing to one content address — the #66 semantics,
1660 /// reachable from CAL.
1661 Occurrence,
1662}
1663
1664// ---------------------------------------------------------------------------
1665// FORMAT spec
1666// ---------------------------------------------------------------------------
1667
1668/// Output format specification (`FORMAT json`, `FORMAT markdown`, etc.).
1669#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1670#[serde(tag = "format", rename_all = "snake_case")]
1671pub enum FormatSpec {
1672 Sml,
1673 Toon,
1674 Markdown,
1675 Json,
1676 Yaml,
1677 Text,
1678 /// Triple output (subject, relation, object per line).
1679 Triples,
1680 /// CSV output (header row + data rows).
1681 Csv,
1682 /// Markdown table output.
1683 Table,
1684 /// A named preset format (e.g. `FORMAT preset "compact"`).
1685 Preset {
1686 name: String,
1687 },
1688 /// A custom template string.
1689 ///
1690 /// `FORMAT TEMPLATE "<text>"` — an inline template in the §10.6.1
1691 /// `ELEMENT` shorthand: the string renders one grain, the engine
1692 /// iterates.
1693 Template {
1694 template: String,
1695 },
1696 /// `FORMAT TEMPLATE <name>` — a reference to a registered template.
1697 ///
1698 /// Distinguished from `Template` by token class, not lookahead: a bare
1699 /// identifier is always a name, a quoted string always a body.
1700 TemplateRef {
1701 name: String,
1702 },
1703 /// `FORMAT TEMPLATE { HEADER { ... } ELEMENT { ... } }` — inline sections.
1704 TemplateInline {
1705 sections: TemplateSectionSources,
1706 },
1707}
1708
1709impl FormatSpec {
1710 /// Return the canonical key name used in multi-format response payloads.
1711 pub fn canonical_key(&self) -> &str {
1712 match self {
1713 Self::Json => "json",
1714 Self::Markdown => "markdown",
1715 Self::Yaml => "yaml",
1716 Self::Text => "text",
1717 Self::Sml => "sml",
1718 Self::Toon => "toon",
1719 Self::Triples => "triples",
1720 Self::Csv => "csv",
1721 Self::Table => "table",
1722 Self::Preset { name } => name.as_str(),
1723 Self::TemplateRef { name } => name.as_str(),
1724 Self::Template { .. } | Self::TemplateInline { .. } => "template",
1725 }
1726 }
1727}
1728
1729/// A format spec with an optional alias for multi-format lists.
1730///
1731/// `FORMAT [json AS customers, TEMPLATE "..." AS users]`
1732///
1733/// When an alias is present, it becomes the key in the `MultiFormatted`
1734/// response payload instead of the canonical format name.
1735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1736pub struct AliasedFormat {
1737 pub spec: FormatSpec,
1738 /// Optional alias (`AS <identifier>`). When `None`, the canonical format
1739 /// name is used as the response key.
1740 #[serde(skip_serializing_if = "Option::is_none")]
1741 pub alias: Option<String>,
1742}
1743
1744/// A FORMAT/AS clause that is either a single format or a list of formats
1745/// (CAL spec v1.0.1, Section 10.1.1).
1746///
1747/// Single format: `FORMAT json` or `AS markdown`
1748/// Multi-format: `FORMAT [markdown, json]` or `AS [markdown, json]`
1749/// Aliased multi: `FORMAT [json AS data, markdown AS readable]`
1750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1751#[serde(tag = "kind", rename_all = "snake_case")]
1752pub enum FormatClause {
1753 /// A single output format (existing behavior).
1754 Single(FormatSpec),
1755 /// Multiple output formats rendered from a single query execution.
1756 /// Maximum 5 formats per list (CAL-E110).
1757 Multi(Vec<AliasedFormat>),
1758}
1759
1760// ---------------------------------------------------------------------------
1761// Grain type names (plural / singular)
1762// ---------------------------------------------------------------------------
1763
1764/// Plural grain type name as used in `RECALL facts`, `RECALL events`, etc.
1765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1766#[serde(rename_all = "snake_case")]
1767pub enum GrainTypePlural {
1768 Facts,
1769 Events,
1770 States,
1771 Workflows,
1772 Tools,
1773 Observations,
1774 Goals,
1775 Reasonings,
1776 Consensuses,
1777 Consents,
1778 Skills,
1779 /// OMS 1.5 / CAL 1.2. Query-only: engine-emitted and lifecycle-gated, so
1780 /// deliberately absent from the addable set (no `ADD recommendation`).
1781 Recommendations,
1782 /// OMS 1.6 §8.13. A standing rule that starts a workflow.
1783 Triggers,
1784 /// Wildcard — `RECALL *` or `RECALL grains` — matches all types.
1785 All,
1786}
1787
1788impl GrainTypePlural {
1789 /// Parse a plural grain type name (case-insensitive).
1790 pub fn parse(s: &str) -> Option<Self> {
1791 match s.to_ascii_lowercase().as_str() {
1792 "facts" | "fact" => Some(Self::Facts),
1793 "events" | "event" => Some(Self::Events),
1794 "states" | "state" => Some(Self::States),
1795 "workflows" | "workflow" => Some(Self::Workflows),
1796 "tools" | "tool" => Some(Self::Tools),
1797 "observations" | "observation" => Some(Self::Observations),
1798 "goals" | "goal" => Some(Self::Goals),
1799 "reasonings" | "reasoning" => Some(Self::Reasonings),
1800 "consensuses" | "consensus" => Some(Self::Consensuses),
1801 "consents" | "consent" => Some(Self::Consents),
1802 "skills" | "skill" => Some(Self::Skills),
1803 "recommendations" | "recommendation" => Some(Self::Recommendations),
1804 "triggers" | "trigger" => Some(Self::Triggers),
1805 "*" | "grains" | "all" => Some(Self::All),
1806 _ => None,
1807 }
1808 }
1809
1810 /// Return the canonical plural string.
1811 pub fn as_str(&self) -> &'static str {
1812 match self {
1813 Self::Facts => "facts",
1814 Self::Events => "events",
1815 Self::States => "states",
1816 Self::Workflows => "workflows",
1817 Self::Tools => "tools",
1818 Self::Observations => "observations",
1819 Self::Goals => "goals",
1820 Self::Reasonings => "reasonings",
1821 Self::Consensuses => "consensuses",
1822 Self::Consents => "consents",
1823 Self::Skills => "skills",
1824 Self::Recommendations => "recommendations",
1825 Self::Triggers => "triggers",
1826 Self::All => "*",
1827 }
1828 }
1829
1830 /// Convert to the engine's `GrainType` enum, if not the wildcard.
1831 pub fn to_grain_type(&self) -> Option<areev_core::types::GrainType> {
1832 match self {
1833 Self::Facts => Some(areev_core::types::GrainType::Fact),
1834 Self::Events => Some(areev_core::types::GrainType::Event),
1835 Self::States => Some(areev_core::types::GrainType::State),
1836 Self::Workflows => Some(areev_core::types::GrainType::Workflow),
1837 Self::Tools => Some(areev_core::types::GrainType::Tool),
1838 Self::Observations => Some(areev_core::types::GrainType::Observation),
1839 Self::Goals => Some(areev_core::types::GrainType::Goal),
1840 Self::Reasonings => Some(areev_core::types::GrainType::Reasoning),
1841 Self::Consensuses => Some(areev_core::types::GrainType::Consensus),
1842 Self::Consents => Some(areev_core::types::GrainType::Consent),
1843 Self::Skills => Some(areev_core::types::GrainType::Skill),
1844 Self::Recommendations => Some(areev_core::types::GrainType::Recommendation),
1845 Self::Triggers => Some(areev_core::types::GrainType::Trigger),
1846 Self::All => None,
1847 }
1848 }
1849}
1850
1851impl std::fmt::Display for GrainTypePlural {
1852 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1853 write!(f, "{}", self.as_str())
1854 }
1855}
1856
1857/// Singular grain type name as used in `ADD fact`, `ADD event`, etc.
1858#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1859#[serde(rename_all = "snake_case")]
1860pub enum GrainTypeSingular {
1861 Fact,
1862 Event,
1863 State,
1864 Workflow,
1865 Tool,
1866 Observation,
1867 Goal,
1868 Reasoning,
1869 Consensus,
1870 Consent,
1871 Skill,
1872 /// OMS 1.5. Query-only — see `GrainTypePlural::Recommendations`.
1873 Recommendation,
1874}
1875
1876impl GrainTypeSingular {
1877 /// Parse a singular grain type name (case-insensitive).
1878 pub fn parse(s: &str) -> Option<Self> {
1879 match s.to_ascii_lowercase().as_str() {
1880 "fact" => Some(Self::Fact),
1881 "event" => Some(Self::Event),
1882 "state" => Some(Self::State),
1883 "workflow" => Some(Self::Workflow),
1884 "tool" => Some(Self::Tool),
1885 "observation" => Some(Self::Observation),
1886 "goal" => Some(Self::Goal),
1887 "reasoning" => Some(Self::Reasoning),
1888 "consensus" => Some(Self::Consensus),
1889 "consent" => Some(Self::Consent),
1890 "skill" => Some(Self::Skill),
1891 "recommendation" => Some(Self::Recommendation),
1892 _ => None,
1893 }
1894 }
1895
1896 /// Return the canonical singular string.
1897 pub fn as_str(&self) -> &'static str {
1898 match self {
1899 Self::Fact => "fact",
1900 Self::Event => "event",
1901 Self::State => "state",
1902 Self::Workflow => "workflow",
1903 Self::Tool => "tool",
1904 Self::Observation => "observation",
1905 Self::Goal => "goal",
1906 Self::Reasoning => "reasoning",
1907 Self::Consensus => "consensus",
1908 Self::Consent => "consent",
1909 Self::Skill => "skill",
1910 Self::Recommendation => "recommendation",
1911 }
1912 }
1913
1914 /// Convert to the engine's `GrainType` enum.
1915 pub fn to_grain_type(&self) -> areev_core::types::GrainType {
1916 match self {
1917 Self::Fact => areev_core::types::GrainType::Fact,
1918 Self::Event => areev_core::types::GrainType::Event,
1919 Self::State => areev_core::types::GrainType::State,
1920 Self::Workflow => areev_core::types::GrainType::Workflow,
1921 Self::Tool => areev_core::types::GrainType::Tool,
1922 Self::Observation => areev_core::types::GrainType::Observation,
1923 Self::Goal => areev_core::types::GrainType::Goal,
1924 Self::Reasoning => areev_core::types::GrainType::Reasoning,
1925 Self::Consensus => areev_core::types::GrainType::Consensus,
1926 Self::Consent => areev_core::types::GrainType::Consent,
1927 Self::Skill => areev_core::types::GrainType::Skill,
1928 Self::Recommendation => areev_core::types::GrainType::Recommendation,
1929 }
1930 }
1931
1932 /// Return the plural form.
1933 pub fn to_plural(&self) -> GrainTypePlural {
1934 match self {
1935 Self::Fact => GrainTypePlural::Facts,
1936 Self::Event => GrainTypePlural::Events,
1937 Self::State => GrainTypePlural::States,
1938 Self::Workflow => GrainTypePlural::Workflows,
1939 Self::Tool => GrainTypePlural::Tools,
1940 Self::Observation => GrainTypePlural::Observations,
1941 Self::Goal => GrainTypePlural::Goals,
1942 Self::Reasoning => GrainTypePlural::Reasonings,
1943 Self::Consensus => GrainTypePlural::Consensuses,
1944 Self::Consent => GrainTypePlural::Consents,
1945 Self::Skill => GrainTypePlural::Skills,
1946 Self::Recommendation => GrainTypePlural::Recommendations,
1947 }
1948 }
1949}
1950
1951impl std::fmt::Display for GrainTypeSingular {
1952 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1953 write!(f, "{}", self.as_str())
1954 }
1955}
1956
1957// ---------------------------------------------------------------------------
1958// Clause types
1959// ---------------------------------------------------------------------------
1960
1961/// `ABOUT "free text query"` — semantic search clause.
1962#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1963pub struct AboutClause {
1964 pub text: String,
1965 #[serde(skip)]
1966 pub span: Option<Span>,
1967}
1968
1969/// `RECENT <n>` — shorthand for ORDER BY created_at DESC LIMIT n.
1970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1971pub struct RecentClause {
1972 pub count: u64,
1973 #[serde(skip)]
1974 pub span: Option<Span>,
1975}
1976
1977/// `SINCE "2024-01-01"` or `SINCE "3 days ago"` — temporal lower-bound.
1978#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1979pub struct SinceClause {
1980 pub expression: String,
1981 #[serde(skip)]
1982 pub span: Option<Span>,
1983}
1984
1985/// `UNTIL "2024-12-31"` or `UNTIL "1 week ago"` — temporal upper-bound.
1986/// Standalone: filters grains with date <= expression.
1987/// Combined with SINCE: forms a date range (SINCE "start" UNTIL "end").
1988#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct UntilClause {
1990 pub expression: String,
1991 #[serde(skip)]
1992 pub span: Option<Span>,
1993}
1994
1995/// `LIKE "example text"` — text-similarity filter.
1996#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1997pub struct LikeClause {
1998 pub text: String,
1999 #[serde(skip)]
2000 pub span: Option<Span>,
2001}
2002
2003/// `BETWEEN "start" AND "end"` — temporal range filter.
2004#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2005pub struct BetweenClause {
2006 pub start: String,
2007 pub end: String,
2008 #[serde(skip)]
2009 pub span: Option<Span>,
2010}
2011
2012/// `CONTRADICTIONS [OF (sub-query)]` — find contradicting grains.
2013/// `CONTRADICTIONS` is a bare terminal per spec; the `OF (sub-query)` tail
2014/// is an Areev extension and is optional.
2015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2016pub struct ContradictionsClause {
2017 pub inner: Option<Box<CalStatement>>,
2018 #[serde(skip)]
2019 pub span: Option<Span>,
2020}
2021
2022// ---------------------------------------------------------------------------
2023// HISTORY DIFF types
2024// ---------------------------------------------------------------------------
2025
2026/// A single field-level difference between two grain versions.
2027///
2028/// Used in `CalResultPayload::Diff` to represent the result of a
2029/// `HISTORY <hash> DIFF <hash>` comparison.
2030#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2031#[serde(tag = "kind", rename_all = "snake_case")]
2032pub enum FieldDiff {
2033 /// A field present in the target grain but absent in the source grain.
2034 Added {
2035 field: String,
2036 value: serde_json::Value,
2037 },
2038 /// A field present in the source grain but absent in the target grain.
2039 Removed {
2040 field: String,
2041 value: serde_json::Value,
2042 },
2043 /// A field present in both grains with different values.
2044 Changed {
2045 field: String,
2046 old: serde_json::Value,
2047 new: serde_json::Value,
2048 },
2049}
2050
2051// ---------------------------------------------------------------------------
2052// LET bindings
2053// ---------------------------------------------------------------------------
2054
2055/// `LET $name = <extractor> OF (sub-query)`
2056#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2057pub struct LetBinding {
2058 /// Parameter name (without the `$` prefix).
2059 pub name: String,
2060 /// The extractor to apply.
2061 pub extractor: Extractor,
2062 /// The sub-query to extract from.
2063 pub source: Box<CalStatement>,
2064 #[serde(skip)]
2065 pub span: Option<Span>,
2066}
2067
2068/// Extractor used in LET bindings and pipeline stages.
2069#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2070#[serde(rename_all = "snake_case")]
2071pub enum Extractor {
2072 /// Extract the `subject` field from each Fact grain.
2073 Subjects,
2074 /// Extract the `object` field from each Fact grain.
2075 Objects,
2076 /// Extract the content-address hash from each grain.
2077 Hashes,
2078}
2079
2080impl std::fmt::Display for Extractor {
2081 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2082 match self {
2083 Self::Subjects => write!(f, "SUBJECTS"),
2084 Self::Objects => write!(f, "OBJECTS"),
2085 Self::Hashes => write!(f, "HASHES"),
2086 }
2087 }
2088}
2089
2090// ---------------------------------------------------------------------------
2091// Tests
2092// ---------------------------------------------------------------------------
2093
2094#[cfg(test)]
2095mod tests {
2096 use super::*;
2097
2098 #[test]
2099 fn test_grain_type_plural_parse() {
2100 assert_eq!(
2101 GrainTypePlural::parse("facts"),
2102 Some(GrainTypePlural::Facts)
2103 );
2104 assert_eq!(
2105 GrainTypePlural::parse("FACTS"),
2106 Some(GrainTypePlural::Facts)
2107 );
2108 assert_eq!(
2109 GrainTypePlural::parse("Events"),
2110 Some(GrainTypePlural::Events)
2111 );
2112 assert_eq!(GrainTypePlural::parse("*"), Some(GrainTypePlural::All));
2113 assert_eq!(GrainTypePlural::parse("grains"), Some(GrainTypePlural::All));
2114 assert_eq!(GrainTypePlural::parse("unknown"), None);
2115 }
2116
2117 #[test]
2118 fn test_grain_type_singular_parse() {
2119 assert_eq!(
2120 GrainTypeSingular::parse("fact"),
2121 Some(GrainTypeSingular::Fact)
2122 );
2123 assert_eq!(
2124 GrainTypeSingular::parse("TOOL"),
2125 Some(GrainTypeSingular::Tool)
2126 );
2127 assert_eq!(GrainTypeSingular::parse("facts"), None); // plural != singular
2128 }
2129
2130 #[test]
2131 fn test_grain_type_plural_to_engine_type() {
2132 let plural = GrainTypePlural::Facts;
2133 assert_eq!(plural.to_grain_type(), Some(areev_core::types::GrainType::Fact));
2134 assert_eq!(GrainTypePlural::All.to_grain_type(), None);
2135 }
2136
2137 #[test]
2138 fn test_grain_type_singular_to_plural() {
2139 assert_eq!(GrainTypeSingular::Fact.to_plural(), GrainTypePlural::Facts);
2140 assert_eq!(
2141 GrainTypeSingular::Consent.to_plural(),
2142 GrainTypePlural::Consents
2143 );
2144 }
2145
2146 #[test]
2147 fn test_comparator_display() {
2148 assert_eq!(format!("{}", Comparator::Eq), "=");
2149 assert_eq!(format!("{}", Comparator::NotEq), "!=");
2150 assert_eq!(format!("{}", Comparator::Gte), ">=");
2151 assert_eq!(format!("{}", Comparator::Lt), "<");
2152 }
2153
2154 #[test]
2155 fn test_value_display() {
2156 assert_eq!(
2157 format!(
2158 "{}",
2159 Value::String {
2160 value: "hello".into()
2161 }
2162 ),
2163 "\"hello\""
2164 );
2165 assert_eq!(format!("{}", Value::Number { value: 42.0 }), "42");
2166 assert_eq!(format!("{}", Value::Boolean { value: true }), "true");
2167 assert_eq!(format!("{}", Value::Parameter { name: "x".into() }), "$x");
2168 assert_eq!(
2169 format!(
2170 "{}",
2171 Value::Hash {
2172 value: "abc123".into()
2173 }
2174 ),
2175 "#abc123"
2176 );
2177 let arr = Value::Array {
2178 values: vec![
2179 Value::String { value: "a".into() },
2180 Value::Number { value: 1.0 },
2181 ],
2182 };
2183 assert_eq!(format!("{}", arr), "[\"a\", 1]");
2184 }
2185
2186 #[test]
2187 fn test_cal_version_default() {
2188 assert_eq!(CalVersion::default(), CalVersion(1));
2189 }
2190
2191 #[test]
2192 fn test_extractor_display() {
2193 assert_eq!(format!("{}", Extractor::Subjects), "SUBJECTS");
2194 assert_eq!(format!("{}", Extractor::Objects), "OBJECTS");
2195 assert_eq!(format!("{}", Extractor::Hashes), "HASHES");
2196 }
2197
2198 #[test]
2199 fn test_set_op_serializes() {
2200 // Verify the serde rename works
2201 let op = SetOp::Intersect;
2202 let json = serde_json::to_string(&op).unwrap();
2203 assert_eq!(json, "\"intersect\"");
2204 }
2205
2206 #[test]
2207 fn test_recall_stmt_construction() {
2208 let stmt = RecallStmt {
2209 grain_type: GrainTypePlural::Facts,
2210 about: Some(AboutClause {
2211 text: "john preferences".into(),
2212 span: None,
2213 }),
2214 where_clause: Some(WhereClause {
2215 condition: Condition::Comparison {
2216 field: "subject".into(),
2217 comparator: Comparator::Eq,
2218 value: Value::String {
2219 value: "john".into(),
2220 },
2221 span: None,
2222 },
2223 span: None,
2224 }),
2225 recent: None,
2226 since: None,
2227 until: None,
2228 like: None,
2229 between: None,
2230 contradictions: None,
2231 limit: Some(10),
2232 as_format: None,
2233 span: None,
2234 };
2235 assert_eq!(stmt.grain_type, GrainTypePlural::Facts);
2236 assert!(stmt.about.is_some());
2237 assert!(stmt.where_clause.is_some());
2238 assert_eq!(stmt.limit, Some(10));
2239 }
2240
2241 #[test]
2242 fn test_cal_query_construction() {
2243 let query = CalQuery {
2244 version: CalVersion(1),
2245 statement: CalStatement::Recall(RecallStmt {
2246 grain_type: GrainTypePlural::Events,
2247 about: None,
2248 where_clause: None,
2249 recent: Some(RecentClause {
2250 count: 5,
2251 span: None,
2252 }),
2253 since: None,
2254 until: None,
2255 like: None,
2256 between: None,
2257 contradictions: None,
2258 limit: None,
2259 as_format: None,
2260 span: None,
2261 }),
2262 pipeline: vec![
2263 PipelineStage::OrderBy {
2264 field: "created_at".into(),
2265 descending: true,
2266 span: None,
2267 },
2268 PipelineStage::Limit {
2269 value: 5,
2270 span: None,
2271 },
2272 ],
2273 with_options: vec![WithOption::ScoreBreakdown],
2274 format: Some(FormatClause::Single(FormatSpec::Json)),
2275 let_bindings: vec![],
2276 let_values: Default::default(),
2277 user_vars: HashMap::new(),
2278 warnings: vec![],
2279 };
2280 assert_eq!(query.version, CalVersion(1));
2281 assert_eq!(query.pipeline.len(), 2);
2282 assert_eq!(query.with_options.len(), 1);
2283 }
2284
2285 #[test]
2286 fn test_nested_condition() {
2287 let cond = Condition::And {
2288 left: Box::new(Condition::Comparison {
2289 field: "subject".into(),
2290 comparator: Comparator::Eq,
2291 value: Value::String {
2292 value: "john".into(),
2293 },
2294 span: None,
2295 }),
2296 right: Box::new(Condition::Or {
2297 left: Box::new(Condition::Comparison {
2298 field: "confidence".into(),
2299 comparator: Comparator::Gte,
2300 value: Value::Number { value: 0.8 },
2301 span: None,
2302 }),
2303 right: Box::new(Condition::IsNotNull {
2304 field: "tags".into(),
2305 span: None,
2306 }),
2307 span: None,
2308 }),
2309 span: None,
2310 };
2311 // Just verify it constructs without panic
2312 match &cond {
2313 Condition::And { .. } => {}
2314 _ => panic!("expected And"),
2315 }
2316 }
2317}