create-feltdb 0.11.4 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
1
+ //! Semantic query composition contract — boundary tests.
2
+ //!
3
+ //! `docs/reference/semantic-query.md` fixes the semantics a FeltDB query
4
+ //! surface must honor when a semantic decision participates in a query. The
5
+ //! bounded authority query implements it (`tests/semantic_query.rs` proves the
6
+ //! page transformation; `e2e/semantic-query-composition.test.mjs` proves the
7
+ //! managed surface). These tests pin the boundaries the contract draws around
8
+ //! the rest of the crate: the canonical query has no semantic vocabulary but
9
+ //! recognizes and refuses a semantic clause rather than dropping it, the
10
+ //! decision transport stays record-scoped, runtime capabilities describe
11
+ //! decisions rather than composition, the record-scoped primitive itself has
12
+ //! no page boundary (the page boundary is `semantic_query`'s), and judgments
13
+ //! stay beside application state. Each test names the section it guards.
14
+
15
+ use feltdb::semantic_decision::{
16
+ decide_record, execute_decision_transport, DecisionDefinition, DecisionError, DecisionOptions,
17
+ DecisionRecordRef, DecisionRequest, DecisionResult, DecisionRuntime,
18
+ DecisionRuntimeCapabilities, DecisionRuntimeMetadata, DecisionTransportExecutionRequest,
19
+ DecisionTransportRuntime, DEFAULT_DECISION_COLLECTION,
20
+ };
21
+ use feltdb::state_contract::{query_hash, CanonicalQuery, QueryFilter};
22
+ use feltdb::FeltDb;
23
+ use serde_json::{json, Value};
24
+ use std::collections::BTreeSet;
25
+ use std::sync::atomic::{AtomicUsize, Ordering};
26
+ use std::sync::Arc;
27
+
28
+ fn memory_db(name: &str) -> FeltDb {
29
+ let path = std::env::temp_dir().join(format!(
30
+ "feltdb-semantic-query-contract-{name}-{}.log",
31
+ std::time::SystemTime::now()
32
+ .duration_since(std::time::UNIX_EPOCH)
33
+ .expect("time")
34
+ .as_nanos()
35
+ ));
36
+ FeltDb::open(path).expect("db")
37
+ }
38
+
39
+ /// A runtime that answers `score` decisions from an `urgency` field and counts
40
+ /// every invocation, so the tests can measure what reaches a provider.
41
+ #[derive(Clone)]
42
+ struct CountingRuntime {
43
+ invocations: Arc<AtomicUsize>,
44
+ }
45
+
46
+ impl CountingRuntime {
47
+ fn new() -> Self {
48
+ Self {
49
+ invocations: Arc::new(AtomicUsize::new(0)),
50
+ }
51
+ }
52
+
53
+ fn invocations(&self) -> usize {
54
+ self.invocations.load(Ordering::SeqCst)
55
+ }
56
+ }
57
+
58
+ impl DecisionRuntime for CountingRuntime {
59
+ fn metadata(&self) -> DecisionRuntimeMetadata {
60
+ DecisionRuntimeMetadata {
61
+ runtime: "deterministic".into(),
62
+ model: Some("deterministic-runtime".into()),
63
+ model_revision: Some("deterministic-v1".into()),
64
+ prompt_revision: Some("prompt-v1".into()),
65
+ execution_method: Some(feltdb::DecisionExecutionMethod::Logit),
66
+ decision_schema_revision: Some("semantic-decision-v1".into()),
67
+ }
68
+ }
69
+
70
+ fn decide(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError> {
71
+ self.invocations.fetch_add(1, Ordering::SeqCst);
72
+ let urgency = request
73
+ .context()
74
+ .get("urgency")
75
+ .and_then(Value::as_str)
76
+ .unwrap_or("low")
77
+ .to_string();
78
+ let levels = ["low", "medium", "high"]
79
+ .into_iter()
80
+ .map(|level| feltdb::WeightedOption {
81
+ value: level.into(),
82
+ probability: if level == urgency { 0.9 } else { 0.05 },
83
+ })
84
+ .collect();
85
+ Ok(DecisionResult::Score {
86
+ selected: urgency,
87
+ levels,
88
+ option_mass: 0.9,
89
+ supporting_fields: vec!["urgency".into()],
90
+ })
91
+ }
92
+ }
93
+
94
+ fn urgency_definition() -> DecisionDefinition {
95
+ DecisionDefinition::Score {
96
+ question: "What is the urgency?".into(),
97
+ levels: vec!["low".into(), "medium".into(), "high".into()],
98
+ }
99
+ }
100
+
101
+ fn seed_tickets(db: &FeltDb, count: usize) {
102
+ for index in 0..count {
103
+ let urgency = ["low", "medium", "high"][index % 3];
104
+ db.insert(
105
+ &format!("tickets:t-{index}"),
106
+ json!({
107
+ "id": format!("t-{index}"),
108
+ "__version": 1,
109
+ "title": format!("Ticket {index}"),
110
+ "urgency": urgency,
111
+ }),
112
+ )
113
+ .expect("insert");
114
+ }
115
+ }
116
+
117
+ /// §7 / §11.6 — the canonical `QueryFilter` vocabulary has no semantic
118
+ /// operator. A filter naming one is refused at deserialization, so no
119
+ /// canonical query can currently express semantic filtering at all.
120
+ #[test]
121
+ fn canonical_query_filter_has_no_semantic_operator() {
122
+ let semantic = json!({
123
+ "operator": "semantic",
124
+ "field": "urgency",
125
+ "decision": { "kind": "binary", "predicate": "is urgent" },
126
+ });
127
+ let error = serde_json::from_value::<QueryFilter>(semantic).expect_err("no semantic operator");
128
+ assert!(
129
+ error.to_string().contains("semantic"),
130
+ "unexpected rejection reason: {error}"
131
+ );
132
+
133
+ // The deterministic vocabulary the contract composes with is intact.
134
+ let deterministic = serde_json::from_value::<QueryFilter>(json!({
135
+ "operator": "eq",
136
+ "field": "urgency",
137
+ "value": "high",
138
+ }))
139
+ .expect("deterministic filter");
140
+ assert!(matches!(deterministic, QueryFilter::Eq { .. }));
141
+ }
142
+
143
+ /// §11.6 — the canonical query recognizes a semantic clause it does not
144
+ /// compose. The clause is carried into the query rather than lost as an
145
+ /// unknown key, so the query's identity changes with it and `execute_query`
146
+ /// can refuse it with `SEMANTIC_UNSUPPORTED` (`state_contract` tests prove
147
+ /// the refusal; `e2e/semantic-query-composition.test.mjs` proves it over
148
+ /// `/v1/query`). The bounded authority query implements the same clause.
149
+ #[test]
150
+ fn canonical_query_recognizes_a_semantic_clause_it_does_not_compose() {
151
+ let plain = json!({
152
+ "collection": "tickets",
153
+ "filter": { "operator": "eq", "field": "urgency", "value": "high" },
154
+ "limit": 10,
155
+ });
156
+ let mut with_clause = plain.clone();
157
+ with_clause["semantic"] = json!({
158
+ "decision": { "kind": "binary", "predicate": "is urgent" },
159
+ "filter": { "is": true },
160
+ });
161
+
162
+ let plain: CanonicalQuery = serde_json::from_value(plain).expect("plain query");
163
+ let with_clause: CanonicalQuery =
164
+ serde_json::from_value(with_clause).expect("the clause is carried, not refused here");
165
+
166
+ assert!(plain.semantic.is_none());
167
+ assert!(with_clause.semantic.is_some());
168
+ assert_ne!(
169
+ query_hash(&plain).expect("hash"),
170
+ query_hash(&with_clause).expect("hash"),
171
+ "the semantic clause is part of query identity"
172
+ );
173
+ // The same clause is exactly what the bounded authority query accepts.
174
+ let clause: feltdb::SemanticQueryClause =
175
+ serde_json::from_value(with_clause.semantic.clone().unwrap()).expect("bounded clause");
176
+ feltdb::validate_semantic_clause(&clause).expect("valid for the bounded query");
177
+ }
178
+
179
+ /// §3 / §5 — the semantic-decision transport addresses exactly one durable
180
+ /// record. There is no candidate-set, page, or collection-level target: a
181
+ /// target without a record id does not deserialize, and the transport refuses
182
+ /// both "no target" and "two targets".
183
+ #[test]
184
+ fn decision_transport_targets_exactly_one_durable_record() {
185
+ let collection_only = serde_json::from_value::<DecisionRecordRef>(json!({
186
+ "collection": "tickets",
187
+ }))
188
+ .expect_err("a collection is not a decision target");
189
+ assert!(
190
+ collection_only.to_string().contains("record_id"),
191
+ "unexpected rejection reason: {collection_only}"
192
+ );
193
+
194
+ let db = memory_db("transport-target");
195
+ seed_tickets(&db, 1);
196
+ let runtime = DecisionTransportRuntime::Recording {
197
+ metadata: CountingRuntime::new().metadata(),
198
+ result: DecisionResult::Score {
199
+ selected: "low".into(),
200
+ levels: vec![],
201
+ option_mass: 0.9,
202
+ supporting_fields: vec![],
203
+ },
204
+ };
205
+ let request = |target: Option<DecisionRecordRef>, context: Option<Value>| {
206
+ DecisionTransportExecutionRequest {
207
+ target,
208
+ context,
209
+ definition: urgency_definition(),
210
+ options: DecisionOptions::default(),
211
+ authorization: None,
212
+ runtime: runtime.clone(),
213
+ }
214
+ };
215
+
216
+ let neither = execute_decision_transport(&db, &request(None, None)).expect_err("no target");
217
+ assert_eq!(neither.code, "DECISION_INVALID_RESULT");
218
+
219
+ let both = execute_decision_transport(
220
+ &db,
221
+ &request(
222
+ Some(DecisionRecordRef {
223
+ collection: "tickets".into(),
224
+ record_id: "t-0".into(),
225
+ }),
226
+ Some(json!({ "urgency": "high" })),
227
+ ),
228
+ )
229
+ .expect_err("two targets");
230
+ assert_eq!(both.code, "DECISION_INVALID_RESULT");
231
+ }
232
+
233
+ /// §14 — runtime capabilities describe decision kinds and batching only. No
234
+ /// capability speaks about candidate sets, ordering, filtering, or cursors,
235
+ /// so a runtime cannot currently advertise query composition and a query
236
+ /// surface cannot currently negotiate it.
237
+ #[test]
238
+ fn runtime_capabilities_declare_no_query_composition() {
239
+ let capabilities =
240
+ serde_json::to_value(DecisionRuntimeCapabilities::default()).expect("capabilities");
241
+ let declared = capabilities
242
+ .as_object()
243
+ .expect("object")
244
+ .keys()
245
+ .cloned()
246
+ .collect::<BTreeSet<_>>();
247
+ let expected = [
248
+ "batch",
249
+ "binary",
250
+ "choice",
251
+ "execution_methods",
252
+ "probability_distribution",
253
+ "score",
254
+ ]
255
+ .into_iter()
256
+ .map(String::from)
257
+ .collect::<BTreeSet<_>>();
258
+ assert_eq!(declared, expected);
259
+ }
260
+
261
+ /// §8 / §13 — the record-scoped primitive has no page boundary of its own:
262
+ /// evaluating N records is N calls, nothing in `semantic_decision` filters or
263
+ /// orders judgments, and results come back in request order. The page boundary,
264
+ /// filtering, and ordering belong to `semantic_query`, which consumes this
265
+ /// primitive and never re-implements it. Reuse is the primitive's own bound:
266
+ /// a second pass costs nothing.
267
+ #[test]
268
+ fn record_scoped_evaluation_has_no_page_boundary_and_no_ordering() {
269
+ let db = memory_db("page-boundary");
270
+ seed_tickets(&db, 6);
271
+ let runtime = CountingRuntime::new();
272
+ let ids = (0..6).map(|index| format!("t-{index}")).collect::<Vec<_>>();
273
+
274
+ let first_pass = ids
275
+ .iter()
276
+ .map(|id| {
277
+ decide_record(
278
+ &db,
279
+ &DecisionRecordRef {
280
+ collection: "tickets".into(),
281
+ record_id: id.clone(),
282
+ },
283
+ &urgency_definition(),
284
+ &runtime,
285
+ &DecisionOptions::default(),
286
+ None,
287
+ )
288
+ .expect("evaluation")
289
+ })
290
+ .collect::<Vec<_>>();
291
+ assert_eq!(runtime.invocations(), ids.len());
292
+ assert!(first_pass.iter().all(|evaluation| !evaluation.cached));
293
+
294
+ let selected = first_pass
295
+ .iter()
296
+ .map(|evaluation| match &evaluation.result {
297
+ DecisionResult::Score { selected, .. } => selected.clone(),
298
+ other => panic!("unexpected result {other:?}"),
299
+ })
300
+ .collect::<Vec<_>>();
301
+ assert_eq!(selected, ["low", "medium", "high", "low", "medium", "high"]);
302
+ let mut ordered = selected.clone();
303
+ ordered.sort_by_key(|level| ["low", "medium", "high"].iter().position(|l| l == level));
304
+ assert_ne!(
305
+ selected, ordered,
306
+ "results come back in request order; nothing in the crate ranks them"
307
+ );
308
+
309
+ for id in &ids {
310
+ let again = decide_record(
311
+ &db,
312
+ &DecisionRecordRef {
313
+ collection: "tickets".into(),
314
+ record_id: id.clone(),
315
+ },
316
+ &urgency_definition(),
317
+ &runtime,
318
+ &DecisionOptions::default(),
319
+ None,
320
+ )
321
+ .expect("reuse");
322
+ assert!(again.cached);
323
+ assert_eq!(again.provenance.source, "cache");
324
+ }
325
+ assert_eq!(runtime.invocations(), ids.len());
326
+ }
327
+
328
+ /// §6 — a judgment is durable derived state in the decision collection. The
329
+ /// application record it judged is untouched: same value, same version. A
330
+ /// semantic query must keep this separation; it may write judgments and
331
+ /// nothing else.
332
+ #[test]
333
+ fn judgments_live_beside_application_state_and_never_inside_it() {
334
+ let db = memory_db("derived-judgment");
335
+ seed_tickets(&db, 1);
336
+ let before: Value = db.get("tickets:t-0").expect("get").expect("record");
337
+
338
+ let evaluation = decide_record(
339
+ &db,
340
+ &DecisionRecordRef {
341
+ collection: "tickets".into(),
342
+ record_id: "t-0".into(),
343
+ },
344
+ &urgency_definition(),
345
+ &CountingRuntime::new(),
346
+ &DecisionOptions::default(),
347
+ None,
348
+ )
349
+ .expect("evaluation");
350
+
351
+ let after: Value = db.get("tickets:t-0").expect("get").expect("record");
352
+ assert_eq!(before, after, "the judged record is not modified");
353
+
354
+ let judgments = db
355
+ .capability(DEFAULT_DECISION_COLLECTION)
356
+ .find(|_: &feltdb::DecisionJudgmentRecord| true)
357
+ .expect("judgments");
358
+ assert_eq!(judgments.len(), 1);
359
+ assert_eq!(judgments[0].id, evaluation.provenance.cache_key);
360
+ assert_eq!(judgments[0].collection, "tickets");
361
+ assert_eq!(judgments[0].record_id, "t-0");
362
+ assert_eq!(judgments[0].state.reference, "tickets:t-0");
363
+ }
@@ -87,6 +87,11 @@ pub struct AppState {
87
87
  pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
88
88
  pub readiness_probe: Arc<PathBuf>,
89
89
  pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
90
+ /// The deployment's semantic runtime binding for bounded semantic query
91
+ /// composition, or `None` when the deployment configures no runtime. A
92
+ /// query never names its runtime; this binding's identity is part of
93
+ /// every judgment the query stage produces or reuses.
94
+ pub semantic_runtime: Option<Arc<dyn feltdb::DecisionRuntime + Send + Sync>>,
90
95
  /// Bounds how many requests may be in flight at once.
91
96
  ///
92
97
  /// Every read and write serializes on one lock inside the database, so