Skip to main content

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}