Skip to main content

graph_storage_sdk/
contract.rs

1//! Executable form of the plugin contracts, behind the `test-support` feature.
2//!
3//! DESIGN says the gear *publishes* the provider contract
4//! (`cpt-cf-graph-storage-contract-embedding-provider`) and ADR-0005 requires
5//! that "contract tests run all three plugins against the provider contract".
6//! A prose contract cannot be run against anything, and a suite living in one
7//! implementation's `tests/` directory cannot be reached by another crate — so
8//! the assertions live here, beside the trait they constrain, and every
9//! provider (the in-process ONNX default, a remote plugin, the deterministic
10//! fake) proves itself against the same code.
11//!
12//! ```ignore
13//! #[tokio::test]
14//! async fn it_honours_the_provider_contract() {
15//!     graph_storage_sdk::contract::assert_embedding_provider(&MyProvider::new()).await;
16//! }
17//! ```
18
19#![allow(
20    clippy::expect_used,
21    reason = "this module is a test harness: a violated clause has to abort \
22              the caller's test with the clause named, and there is no other \
23              outcome for it to return"
24)]
25
26use std::time::Duration;
27
28use tokio_util::sync::CancellationToken;
29
30use crate::models::RemainingBudget;
31use crate::plugin_api::{EmbedRequest, EmbeddingProviderError, EmbeddingProviderV1};
32
33fn request(inputs: Vec<String>) -> EmbedRequest {
34    EmbedRequest {
35        inputs,
36        budget: RemainingBudget::starting_now(Duration::from_secs(30)),
37        cancel: CancellationToken::new(),
38    }
39}
40
41/// Assert that `provider` honours [`EmbeddingProviderV1`].
42///
43/// Panics with a message naming the broken clause. Written as assertions
44/// rather than a returned report because a provider that fails any of these
45/// cannot be deployed at all: there is nothing to triage.
46///
47/// # Panics
48///
49/// Whenever the provider violates the contract.
50pub async fn assert_embedding_provider<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
51    assert_declaration(provider);
52    assert_alignment_and_width(provider).await;
53    assert_determinism(provider).await;
54    assert_empty_batch(provider).await;
55    assert_budget_and_cancellation(provider).await;
56
57    provider
58        .health()
59        .await
60        .expect("a provider that cannot answer `health` cannot be made ready");
61}
62
63/// What the provider says about itself before it is asked to do anything.
64fn assert_declaration<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
65    let space = provider.embedding_space();
66    assert_eq!(
67        space.dimension,
68        provider.dimension(),
69        "dimension() and embedding_space().dimension describe one space and must agree"
70    );
71    assert!(
72        provider.dimension() > 0,
73        "a zero-width space cannot rank anything"
74    );
75    assert!(
76        !space.identity_hash.is_empty(),
77        "the identity hash is what readiness compares against; an empty one \
78         makes every space look alike"
79    );
80}
81
82/// Two inputs are never enough: three, including an empty string, is what
83/// catches a provider that drops a degenerate input instead of embedding it
84/// and thereby shifts every vector after it onto the wrong node.
85fn sample_inputs() -> Vec<String> {
86    vec![
87        "the first input".to_owned(),
88        "a second, quite different input".to_owned(),
89        String::new(),
90    ]
91}
92
93async fn assert_alignment_and_width<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
94    let dimension = provider.dimension() as usize;
95    let inputs = sample_inputs();
96    let response = provider
97        .embed(request(inputs.clone()))
98        .await
99        .expect("a provider must embed a well-formed batch");
100
101    assert_eq!(
102        response.vectors.len(),
103        inputs.len(),
104        "vectors are aligned with inputs by index, so a short answer is a \
105         silent mis-assignment of every vector after the gap"
106    );
107    for (index, vector) in response.vectors.iter().enumerate() {
108        assert_eq!(
109            vector.len(),
110            dimension,
111            "vector {index} is {} wide against a declared width of {dimension}",
112            vector.len()
113        );
114        assert!(
115            vector.iter().all(|lane| lane.is_finite()),
116            "vector {index} carries a NaN or an infinity, which no distance \
117             operator can order"
118        );
119    }
120    assert_eq!(
121        &response.space,
122        provider.embedding_space(),
123        "the echoed space must be the declared one, or a mismatch is only \
124         discoverable at configuration time"
125    );
126}
127
128/// How far two embeddings of the same text may diverge, as `1 - cosine`.
129///
130/// Well above the drift of nondeterministic floating-point reduction on
131/// unit vectors, which sits around `1e-6`, and well below what separates two
132/// different texts: at `1e-4` a provider that answers a different direction
133/// the second time still fails.
134const DETERMINISM_TOLERANCE: f32 = 1e-4;
135
136/// What is wrong, if anything, with two embeddings of the same batch.
137fn determinism_violation(first: &[Vec<f32>], second: &[Vec<f32>]) -> Option<String> {
138    if first.len() != second.len() {
139        return Some(format!(
140            "the same batch answered {} vectors and then {}",
141            first.len(),
142            second.len()
143        ));
144    }
145    for (index, (a, b)) in first.iter().zip(second).enumerate() {
146        if a.len() != b.len() {
147            return Some(format!("vector {index} changed width between calls"));
148        }
149        let similarity = cosine(a, b);
150        if similarity < 1.0 - DETERMINISM_TOLERANCE {
151            return Some(format!(
152                "vector {index} drifted to cosine {similarity} of itself between two calls \
153                 with the same input; the same text must embed to the same direction"
154            ));
155        }
156    }
157    None
158}
159
160fn cosine(a: &[f32], b: &[f32]) -> f32 {
161    let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
162    let norm = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
163    let denominator = norm(a) * norm(b);
164    if denominator == 0.0 {
165        // Two zero vectors point nowhere; equal is the only honest reading.
166        return if a == b { 1.0 } else { 0.0 };
167    }
168    dot / denominator
169}
170
171async fn assert_determinism<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
172    let inputs = sample_inputs();
173    let first = provider
174        .embed(request(inputs.clone()))
175        .await
176        .expect("a provider must embed a well-formed batch");
177    let second = provider
178        .embed(request(inputs))
179        .await
180        .expect("a provider must embed a well-formed batch");
181
182    // The same text must embed to the same *direction*: ingest and query
183    // embed at different times, and a provider whose second answer points
184    // elsewhere ranks a document below its own text. Bit-for-bit equality is
185    // more than that asks, and more than ADR-0004 asks -- it wants
186    // determinism of the fake, for CI's sake, not of every provider. GPU
187    // inference and multi-threaded BLAS reduce in a nondeterministic order
188    // and drift in the last few bits, which moves no document relative to
189    // its own text; an exact comparison would have refused every such
190    // provider on a property the gear never relies on. Nothing downstream
191    // compares vectors exactly either: re-embedding is decided by a hash of
192    // the input text, not of the vector.
193    if let Some(problem) = determinism_violation(&first.vectors, &second.vectors) {
194        panic!("{problem}");
195    }
196    assert_ne!(
197        first.vectors.first(),
198        first.vectors.get(1),
199        "two unrelated inputs embedded identically; a provider that answers a \
200         constant passes every other clause here"
201    );
202}
203
204async fn assert_empty_batch<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
205    let empty = provider
206        .embed(request(Vec::new()))
207        .await
208        .expect("an empty batch is a no-op, not an error");
209    assert!(
210        empty.vectors.is_empty(),
211        "an empty batch produced {} vectors",
212        empty.vectors.len()
213    );
214}
215
216/// What every provider can be held to: a call that is already over is
217/// refused before any work.
218///
219/// Deliberately not "cancellation is honoured mid-batch", which is not a
220/// clause this contract can make universal -- providers do not share a unit of
221/// work. The ONNX one turns a whole batch into a single inference and can
222/// only check around it; the remote one sends chunks and checks between them;
223/// the in-memory one hashes item by item and checks between those. Each
224/// boundary is asserted where it exists, in that provider's own tests, because
225/// only there is it known where the boundary is.
226async fn assert_budget_and_cancellation<P: EmbeddingProviderV1 + ?Sized>(provider: &P) {
227    let exhausted = EmbedRequest {
228        inputs: vec!["anything".to_owned()],
229        budget: RemainingBudget::starting_now(Duration::ZERO),
230        cancel: CancellationToken::new(),
231    };
232    assert!(
233        matches!(
234            provider.embed(exhausted).await,
235            Err(EmbeddingProviderError::Deadline)
236        ),
237        "an exhausted budget must be refused as `Deadline`, not served late: \
238         the caller's deadline is absolute and already spent"
239    );
240
241    let cancel = CancellationToken::new();
242    cancel.cancel();
243    let cancelled = EmbedRequest {
244        inputs: vec!["anything".to_owned()],
245        budget: RemainingBudget::starting_now(Duration::from_secs(30)),
246        cancel,
247    };
248    assert!(
249        matches!(
250            provider.embed(cancelled).await,
251            Err(EmbeddingProviderError::Cancelled)
252        ),
253        "a cancelled call must be refused as `Cancelled`"
254    );
255}
256
257#[cfg(test)]
258mod tests {
259    use super::determinism_violation;
260
261    fn unit(v: &[f32]) -> Vec<f32> {
262        let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
263        v.iter().map(|x| x / norm).collect()
264    }
265
266    /// The drift nondeterministic reduction produces -- last-bits noise on a
267    /// unit vector -- is not a violation. An exact comparison refused it.
268    #[test]
269    fn last_bits_drift_is_the_same_embedding() {
270        let first = vec![unit(&[0.3, 0.5, 0.8, 0.1])];
271        let second: Vec<Vec<f32>> = first
272            .iter()
273            .map(|v| v.iter().map(|x| x + 1e-6).collect())
274            .collect();
275        assert_ne!(first, second, "the fixture must actually differ");
276        assert_eq!(determinism_violation(&first, &second), None);
277    }
278
279    /// A provider whose second answer points somewhere else still fails:
280    /// that is the property the clause exists for.
281    #[test]
282    fn a_different_direction_is_not_the_same_embedding() {
283        let first = vec![unit(&[1.0, 0.0, 0.0, 0.0])];
284        let second = vec![unit(&[0.9, 0.3, 0.0, 0.0])];
285        let problem =
286            determinism_violation(&first, &second).expect("a vector that moved is refused");
287        assert!(problem.contains("drifted"), "{problem}");
288    }
289
290    /// A zero vector points nowhere, so the cosine is undefined; the check
291    /// reads two zero vectors as the same embedding and a zero against
292    /// anything else as a different one, rather than dividing by zero.
293    #[test]
294    fn a_zero_vector_is_the_same_only_as_another_zero_vector() {
295        let zero = vec![vec![0.0_f32, 0.0, 0.0]];
296        assert_eq!(
297            determinism_violation(&zero, &zero),
298            None,
299            "a provider that answers a zero vector twice is deterministic"
300        );
301        let other = vec![unit(&[1.0, 0.0, 0.0])];
302        assert!(
303            determinism_violation(&zero, &other).is_some(),
304            "a zero vector and then a real one is not the same embedding"
305        );
306        assert!(
307            determinism_violation(&other, &zero).is_some(),
308            "nor the other way round"
309        );
310    }
311
312    #[test]
313    fn a_changed_shape_is_refused() {
314        let one = vec![unit(&[1.0, 2.0])];
315        assert!(
316            determinism_violation(&one, &[]).is_some(),
317            "a vector went missing"
318        );
319        assert!(
320            determinism_violation(&one, &[unit(&[1.0, 2.0, 3.0])]).is_some(),
321            "a vector changed width"
322        );
323    }
324}