lunaris_core/storage/capabilities.rs
1//! `StorageCapabilities` — what the backend supports natively.
2//!
3//! Higher layers (retrievers, recipes, the conformance suite) read this struct to
4//! decide whether to degrade gracefully (e.g., skip rerank when `rerank_native=false`)
5//! or refuse a query path (e.g., reject Cypher when `graph_native=false`).
6
7use serde::{Deserialize, Serialize};
8
9/// Cypher dialect tier the backend's graph executor accepts.
10///
11/// Wave 4 amendment: a parallel-agent probe found that Moon does not
12/// accept the full Wave-4 template. It rejects
13/// `MATCH p = (n)-[*1..N]-(m)` (path-variable binding is restricted to
14/// `shortestPath()` calls) and its function table omits `length()`,
15/// `reduce()`, and `relationships()`. Primary-source citation:
16/// `vendor/moon/src/graph/cypher/parser/pattern.rs:172-216`
17/// + `executor/eval.rs:116-227`.
18///
19/// Each backend declares the tier it genuinely supports via
20/// [`StorageCapabilities::cypher_dialect`]. The
21/// `crates/lunaris-retrieve/src/operators/graph.rs` operator reads this
22/// field at retrieve-time and picks the matching template. Promotion to
23/// a higher tier requires primary-source dialect verification.
24///
25/// 0.7.0 removed the intermediate `PathMetrics` tier — it existed only to
26/// describe Apache AGE 1.5 (path binding + `length(p)`, but no `reduce()`),
27/// and the Postgres backend that spoke it was deleted with the Moon-only
28/// break. No producer survived it.
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
30pub enum CypherDialect {
31 /// Pre-Wave-4 template: `MATCH (n)-[*1..N]-(m) RETURN m.id_hex AS id,
32 /// m.name AS name, m.type AS type`. No path metrics, no source
33 /// entity. This is Moon's ceiling today. The operator's
34 /// optional-header fallback (Wave 3) reduces the score formula to the
35 /// legacy `1.0 / (1.0 + i)` when no path metrics arrive.
36 #[default]
37 Legacy,
38 /// Wave 4 full: `MATCH p = ...` path binding + `length(p) AS
39 /// path_length` + `n.id_hex AS source_entity_id` +
40 /// `reduce(w=1.0, r in relationships(p) | w * coalesce(r.weight, 1.0))
41 /// AS edge_weight_product`. No current backend supports this — kept
42 /// for forward-compat against a Moon that grows path binding and
43 /// `reduce()` over variable-length paths.
44 Full,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub struct StorageCapabilities {
49 /// `true` only when KV reads support historical `AS_OF` natively.
50 ///
51 /// Moon supports temporal graph and FT surfaces, but its hash/KV `HGET`
52 /// path currently returns latest state for Lunaris KV rows, so Moon
53 /// reports `false` and `read_as_of` refuses a historical pin with
54 /// `NotSupported` rather than silently answering with current state.
55 pub bi_temporal_native: bool,
56 /// `true` for Moon (CSR + Cypher).
57 pub graph_native: bool,
58 /// `true` for Moon's bundled cross-encoder.
59 pub rerank_native: bool,
60 /// `true` for Moon (`MQ.*`).
61 pub queue_native: bool,
62 /// Maximum vector dimension the backend's index can hold. Moon reports the
63 /// dimension its FT indices were actually created at (768 by default).
64 pub max_vector_dim: u32,
65 /// `true` when the backend can run RRF (Reciprocal Rank Fusion) over
66 /// `(vector, sparse_bm25)` natively in a single round trip — i.e., Moon's
67 /// `FT.SEARCH ... HYBRID VECTOR ... SPARSE ... FUSION RRF WEIGHTS ...` exposed
68 /// via `client.text().hybrid_search()`. Phase 2's `fuse_rrf` operator opts into
69 /// `RrfFusion::Moon` when both branches hit a backend with `native_rrf=true` AND
70 /// both branches are `Vector` / `Keyword(BM25)` operators on the same Moon index.
71 /// Backends with `native_rrf=false` fall back to client-side fusion
72 /// (`RrfFusion::Client`), which is always correct if slower.
73 pub native_rrf: bool,
74 /// Recommended upper bound on active scopes for this backend.
75 ///
76 /// Moon creates one FT index + one graph key + N MQ topics **per scope**.
77 /// Moon's soft limit is ~512 FT indices per node before recall p99 degrades
78 /// (per Moon docs §6.4 "index count"). Above `max_scopes_recommended` the
79 /// operator should consider workspace-level pooling (future RFC). A value of
80 /// `0` means no limit is documented — a backend with no per-scope index
81 /// multiplier.
82 ///
83 /// RFC 0001 §3.6: set to `512` for Moon.
84 pub max_scopes_recommended: usize,
85 /// Cypher dialect tier the backend's graph executor accepts.
86 ///
87 /// Defaults to [`CypherDialect::Legacy`] — the universally-supported
88 /// pre-Wave-4 template. Backends opt into `Full` only with
89 /// primary-source dialect verification; Moon stays at `Legacy` (the
90 /// Wave 4 probe rejected `MATCH p = ...`).
91 ///
92 /// See [`CypherDialect`] for the full tier matrix.
93 pub cypher_dialect: CypherDialect,
94 /// `true` when the backend's graph executor supports native recency-decay
95 /// traversal — i.e., Moon's `GRAPH.QUERY ... --decay <λ> [--time-weight <w>]`
96 /// read-path clause (effective edge cost `|weight| + λ·w·age_seconds`).
97 ///
98 /// Callers gate `StoragePort::graph_traverse_decayed(decay: Some(_))` on
99 /// this flag; the default trait impl returns `NotSupported`. `true` for
100 /// Moon (v0.3.0+). `#[serde(default)]` keeps pre-v0.7 serialized
101 /// capability payloads parseable (missing field → `false`).
102 #[serde(default)]
103 pub graph_decay_native: bool,
104 /// `true` when the backend supports graph-expanded vector retrieval —
105 /// i.e., Moon's `FT.NAVIGATE` (KNN seeds → bounded BFS → hop-aware
106 /// re-rank). Callers gate `StoragePort::vector_navigate` on this flag;
107 /// the DSL `Navigate` operator degrades to plain `vector_search` when
108 /// `false`. `true` for Moon (v0.3.0+). `#[serde(default)]` keeps older
109 /// serialized payloads parseable.
110 #[serde(default)]
111 pub graph_navigate_native: bool,
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn cypher_dialect_default_is_legacy() {
120 // Default tier MUST be Legacy — universally-supported template.
121 // Backends opt into higher tiers explicitly; new backends (or
122 // test fixtures that build via `..Default::default()`) MUST NOT
123 // silently promote.
124 assert_eq!(CypherDialect::default(), CypherDialect::Legacy);
125 }
126}