1mod cross_source;
9mod edge;
10pub mod file_catalog;
11mod meta;
12mod node;
13mod path_id;
14mod queries;
15mod schema;
16pub mod snapshot;
17mod sync;
18
19pub use edge::{Edge, EdgeKind};
20pub use file_catalog::FileCatalogEntry;
21pub use meta::{PropertyGraphMetaV1, load_meta, meta_path, write_meta};
22pub use node::{Node, NodeKind};
23pub use queries::{
24 DependencyChain, GraphQuery, ImpactResult, edge_weight, file_connectivity, related_files,
25};
26pub use sync::{mirror_index, parse_symbol_metadata, populate_from_project_index};
27
28use rusqlite::Connection;
29use std::path::{Path, PathBuf};
30
31pub fn graph_dir(project_root: &str) -> PathBuf {
37 if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
38 let normalized = crate::core::graph_index::normalize_project_root(project_root);
39 let hash = crate::core::project_hash::hash_project_root(&normalized);
40 data_dir.join("graphs").join(hash)
41 } else {
42 Path::new(project_root).join(".lean-ctx")
43 }
44}
45
46fn migrate_if_needed(project_root: &str, new_dir: &Path) {
49 let old_dir = Path::new(project_root).join(".lean-ctx");
50 if old_dir == new_dir {
51 return;
52 }
53 for file in &["graph.db", "graph.meta.json"] {
54 let old = old_dir.join(file);
55 let new = new_dir.join(file);
56 if old.exists()
57 && !new.exists()
58 && std::fs::rename(&old, &new).is_err()
59 && std::fs::copy(&old, &new).is_ok()
60 {
61 let _ = std::fs::remove_file(&old);
62 }
63 }
64}
65
66pub const GRAPH_ENGINE_VERSION: u32 = 5;
82
83pub fn engine_outdated(project_root: &str) -> bool {
89 load_meta(project_root).is_none_or(|m| m.engine_version < GRAPH_ENGINE_VERSION)
90}
91
92fn is_transient_lock(err: &anyhow::Error) -> bool {
97 use rusqlite::ErrorCode;
98 if let Some(rusqlite::Error::SqliteFailure(e, _)) = err.downcast_ref::<rusqlite::Error>()
99 && matches!(e.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked)
100 {
101 return true;
102 }
103 let msg = format!("{err:#}").to_ascii_lowercase();
106 msg.contains("database is locked") || msg.contains("table is locked")
107}
108
109pub struct CodeGraph {
110 conn: Connection,
111 db_path: PathBuf,
112}
113
114impl CodeGraph {
115 pub fn open(project_root: &str) -> anyhow::Result<Self> {
116 let db_dir = graph_dir(project_root);
117 std::fs::create_dir_all(&db_dir)?;
118 migrate_if_needed(project_root, &db_dir);
119 let db_path = db_dir.join("graph.db");
120
121 const MAX_ATTEMPTS: u32 = 12;
128 let mut attempt = 0;
129 loop {
130 attempt += 1;
131 match Self::try_open(&db_path) {
132 Ok(graph) => return Ok(graph),
133 Err(e) if attempt < MAX_ATTEMPTS && is_transient_lock(&e) => {
134 std::thread::sleep(std::time::Duration::from_millis(40 * u64::from(attempt)));
135 }
136 Err(e) => return Err(e),
137 }
138 }
139 }
140
141 fn try_open(db_path: &Path) -> anyhow::Result<Self> {
143 let conn = Connection::open(db_path)?;
144 conn.busy_timeout(std::time::Duration::from_secs(5))?;
145 schema::initialize(&conn)?;
146 Ok(Self {
147 conn,
148 db_path: db_path.to_path_buf(),
149 })
150 }
151
152 pub fn open_in_memory() -> anyhow::Result<Self> {
153 let conn = Connection::open_in_memory()?;
154 schema::initialize(&conn)?;
155 Ok(Self {
156 conn,
157 db_path: PathBuf::from(":memory:"),
158 })
159 }
160
161 pub fn db_path(&self) -> &Path {
162 &self.db_path
163 }
164
165 pub fn connection(&self) -> &Connection {
166 &self.conn
167 }
168
169 pub fn upsert_node(&self, node: &Node) -> anyhow::Result<i64> {
170 node::upsert(&self.conn, node)
171 }
172
173 pub fn upsert_edge(&self, edge: &Edge) -> anyhow::Result<()> {
174 edge::upsert(&self.conn, edge)
175 }
176
177 pub fn get_node_by_path(&self, file_path: &str) -> anyhow::Result<Option<Node>> {
178 node::get_by_path(&self.conn, file_path)
179 }
180
181 pub fn get_node_by_symbol(&self, name: &str, file_path: &str) -> anyhow::Result<Option<Node>> {
182 node::get_by_symbol(&self.conn, name, file_path)
183 }
184
185 pub fn remove_file_nodes(&self, file_path: &str) -> anyhow::Result<()> {
186 node::remove_by_file(&self.conn, file_path)
187 }
188
189 pub fn edges_from(&self, node_id: i64) -> anyhow::Result<Vec<Edge>> {
190 edge::from_node(&self.conn, node_id)
191 }
192
193 pub fn edges_to(&self, node_id: i64) -> anyhow::Result<Vec<Edge>> {
194 edge::to_node(&self.conn, node_id)
195 }
196
197 pub fn dependents(&self, file_path: &str) -> anyhow::Result<Vec<String>> {
198 queries::dependents(&self.conn, file_path)
199 }
200
201 pub fn dependencies(&self, file_path: &str) -> anyhow::Result<Vec<String>> {
202 queries::dependencies(&self.conn, file_path)
203 }
204
205 pub fn impact_analysis(
206 &self,
207 file_path: &str,
208 max_depth: usize,
209 ) -> anyhow::Result<ImpactResult> {
210 queries::impact_analysis(&self.conn, file_path, max_depth)
211 }
212
213 pub fn dependency_chain(
214 &self,
215 from: &str,
216 to: &str,
217 ) -> anyhow::Result<Option<DependencyChain>> {
218 queries::dependency_chain(&self.conn, from, to)
219 }
220
221 pub fn related_files(
222 &self,
223 file_path: &str,
224 limit: usize,
225 ) -> anyhow::Result<Vec<(String, f64)>> {
226 queries::related_files(&self.conn, file_path, limit)
227 }
228
229 pub fn file_connectivity(
230 &self,
231 file_path: &str,
232 ) -> anyhow::Result<std::collections::HashMap<String, (usize, usize)>> {
233 queries::file_connectivity(&self.conn, file_path)
234 }
235
236 pub fn node_count(&self) -> anyhow::Result<usize> {
237 node::count(&self.conn)
238 }
239
240 pub fn edge_count(&self) -> anyhow::Result<usize> {
241 edge::count(&self.conn)
242 }
243
244 pub fn upsert_cross_source_edge(
248 &self,
249 from: &str,
250 to: &str,
251 kind: &str,
252 weight: f32,
253 ) -> anyhow::Result<()> {
254 cross_source::upsert(&self.conn, from, to, kind, weight)
255 }
256
257 pub fn all_cross_source_edges(&self) -> Vec<crate::core::graph_index::IndexEdge> {
259 cross_source::all(&self.conn).unwrap_or_default()
260 }
261
262 pub fn cross_source_edge_count(&self) -> anyhow::Result<usize> {
263 cross_source::count(&self.conn)
264 }
265
266 pub fn delete_cross_source_edges_by_kind(&self, kind: &str) -> anyhow::Result<usize> {
270 cross_source::delete_by_kind(&self.conn, kind)
271 }
272
273 pub fn clear(&self) -> anyhow::Result<()> {
274 self.conn.execute_batch(
275 "DELETE FROM edges; DELETE FROM nodes; DELETE FROM file_catalog; DELETE FROM paths; \
276 DELETE FROM cross_source_edges;",
277 )?;
278 Ok(())
279 }
280
281 pub fn clear_code_graph(&self) -> anyhow::Result<()> {
286 self.conn.execute_batch(
287 "DELETE FROM edges; DELETE FROM nodes; DELETE FROM file_catalog; DELETE FROM paths;",
288 )?;
289 Ok(())
290 }
291
292 pub fn upsert_file_catalog(&self, entry: &FileCatalogEntry) -> anyhow::Result<()> {
293 file_catalog::upsert(&self.conn, entry)
294 }
295
296 pub fn get_file_catalog(&self, path: &str) -> anyhow::Result<Option<FileCatalogEntry>> {
297 file_catalog::get(&self.conn, path)
298 }
299
300 pub fn file_catalog_count(&self) -> anyhow::Result<usize> {
301 file_catalog::count(&self.conn)
302 }
303
304 pub fn file_catalog_paths(&self) -> anyhow::Result<Vec<String>> {
305 file_catalog::all_paths(&self.conn)
306 }
307
308 pub fn find_symbols(
309 &self,
310 name: &str,
311 file_filter: Option<&str>,
312 kind_filter: Option<&str>,
313 ) -> anyhow::Result<Vec<Node>> {
314 node::find_symbols(&self.conn, name, file_filter, kind_filter)
315 }
316
317 pub fn resolve_symbol_def_files(&self, name: &str) -> anyhow::Result<Vec<String>> {
320 node::resolve_symbol_def_files(&self.conn, name)
321 }
322
323 pub fn symbol_count(&self) -> anyhow::Result<usize> {
324 node::symbol_count(&self.conn)
325 }
326
327 pub fn file_node_count(&self) -> anyhow::Result<usize> {
330 node::file_count(&self.conn)
331 }
332
333 pub fn all_symbols(&self) -> anyhow::Result<Vec<Node>> {
336 node::all_symbols(&self.conn)
337 }
338
339 pub fn all_edges_flat(&self) -> anyhow::Result<Vec<(String, String, String, f64)>> {
340 node::all_edges_flat(&self.conn)
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::core::data_dir::test_env_lock;
348
349 fn test_graph() -> CodeGraph {
350 CodeGraph::open_in_memory().unwrap()
351 }
352
353 #[test]
354 fn create_and_query_nodes() {
355 let g = test_graph();
356
357 let id = g.upsert_node(&Node::file("src/main.rs")).unwrap();
358 assert!(id > 0);
359
360 let found = g.get_node_by_path("src/main.rs").unwrap();
361 assert!(found.is_some());
362 assert_eq!(found.unwrap().file_path, "src/main.rs");
363 }
364
365 #[test]
366 fn create_and_query_edges() {
367 let g = test_graph();
368
369 let a = g.upsert_node(&Node::file("src/a.rs")).unwrap();
370 let b = g.upsert_node(&Node::file("src/b.rs")).unwrap();
371
372 g.upsert_edge(&Edge::new(a, b, EdgeKind::Imports)).unwrap();
373
374 let from_a = g.edges_from(a).unwrap();
375 assert_eq!(from_a.len(), 1);
376 assert_eq!(from_a[0].target_id, b);
377
378 let to_b = g.edges_to(b).unwrap();
379 assert_eq!(to_b.len(), 1);
380 assert_eq!(to_b[0].source_id, a);
381 }
382
383 #[test]
384 fn dependents_query() {
385 let g = test_graph();
386
387 let main = g.upsert_node(&Node::file("src/main.rs")).unwrap();
388 let lib = g.upsert_node(&Node::file("src/lib.rs")).unwrap();
389 let utils = g.upsert_node(&Node::file("src/utils.rs")).unwrap();
390
391 g.upsert_edge(&Edge::new(main, lib, EdgeKind::Imports))
392 .unwrap();
393 g.upsert_edge(&Edge::new(utils, lib, EdgeKind::Imports))
394 .unwrap();
395
396 let deps = g.dependents("src/lib.rs").unwrap();
397 assert_eq!(deps.len(), 2);
398 assert!(deps.contains(&"src/main.rs".to_string()));
399 assert!(deps.contains(&"src/utils.rs".to_string()));
400 }
401
402 #[test]
403 fn dependencies_query() {
404 let g = test_graph();
405
406 let main = g.upsert_node(&Node::file("src/main.rs")).unwrap();
407 let lib = g.upsert_node(&Node::file("src/lib.rs")).unwrap();
408 let config = g.upsert_node(&Node::file("src/config.rs")).unwrap();
409
410 g.upsert_edge(&Edge::new(main, lib, EdgeKind::Imports))
411 .unwrap();
412 g.upsert_edge(&Edge::new(main, config, EdgeKind::Imports))
413 .unwrap();
414
415 let deps = g.dependencies("src/main.rs").unwrap();
416 assert_eq!(deps.len(), 2);
417 }
418
419 #[test]
420 #[allow(clippy::many_single_char_names)] fn impact_analysis_depth() {
422 let g = test_graph();
423
424 let a = g.upsert_node(&Node::file("a.rs")).unwrap();
425 let b = g.upsert_node(&Node::file("b.rs")).unwrap();
426 let c = g.upsert_node(&Node::file("c.rs")).unwrap();
427 let d = g.upsert_node(&Node::file("d.rs")).unwrap();
428
429 g.upsert_edge(&Edge::new(b, a, EdgeKind::Imports)).unwrap();
430 g.upsert_edge(&Edge::new(c, b, EdgeKind::Imports)).unwrap();
431 g.upsert_edge(&Edge::new(d, c, EdgeKind::Imports)).unwrap();
432
433 let impact = g.impact_analysis("a.rs", 2).unwrap();
434 assert!(impact.affected_files.contains(&"b.rs".to_string()));
435 assert!(impact.affected_files.contains(&"c.rs".to_string()));
436 assert!(!impact.affected_files.contains(&"d.rs".to_string()));
437
438 let deep = g.impact_analysis("a.rs", 10).unwrap();
439 assert!(deep.affected_files.contains(&"d.rs".to_string()));
440 }
441
442 #[test]
443 fn upsert_idempotent() {
444 let g = test_graph();
445
446 let id1 = g.upsert_node(&Node::file("src/main.rs")).unwrap();
447 let id2 = g.upsert_node(&Node::file("src/main.rs")).unwrap();
448 assert_eq!(id1, id2);
449 assert_eq!(g.node_count().unwrap(), 1);
450 }
451
452 #[test]
453 fn remove_file_cascades() {
454 let g = test_graph();
455
456 let a = g.upsert_node(&Node::file("src/a.rs")).unwrap();
457 let b = g.upsert_node(&Node::file("src/b.rs")).unwrap();
458 let sym = g
459 .upsert_node(&Node::symbol("MyStruct", "src/a.rs", NodeKind::Symbol))
460 .unwrap();
461
462 g.upsert_edge(&Edge::new(a, b, EdgeKind::Imports)).unwrap();
463 g.upsert_edge(&Edge::new(sym, b, EdgeKind::Calls)).unwrap();
464
465 g.remove_file_nodes("src/a.rs").unwrap();
466
467 assert!(g.get_node_by_path("src/a.rs").unwrap().is_none());
468 assert_eq!(g.edge_count().unwrap(), 0);
469 }
470
471 #[test]
472 fn dependency_chain_found() {
473 let g = test_graph();
474
475 let a = g.upsert_node(&Node::file("a.rs")).unwrap();
476 let b = g.upsert_node(&Node::file("b.rs")).unwrap();
477 let c = g.upsert_node(&Node::file("c.rs")).unwrap();
478
479 g.upsert_edge(&Edge::new(a, b, EdgeKind::Imports)).unwrap();
480 g.upsert_edge(&Edge::new(b, c, EdgeKind::Imports)).unwrap();
481
482 let chain = g.dependency_chain("a.rs", "c.rs").unwrap();
483 assert!(chain.is_some());
484 let chain = chain.unwrap();
485 assert_eq!(chain.path, vec!["a.rs", "b.rs", "c.rs"]);
486 }
487
488 #[test]
489 fn counts() {
490 let g = test_graph();
491 assert_eq!(g.node_count().unwrap(), 0);
492 assert_eq!(g.edge_count().unwrap(), 0);
493
494 let a = g.upsert_node(&Node::file("a.rs")).unwrap();
495 let b = g.upsert_node(&Node::file("b.rs")).unwrap();
496 g.upsert_edge(&Edge::new(a, b, EdgeKind::Imports)).unwrap();
497
498 assert_eq!(g.node_count().unwrap(), 2);
499 assert_eq!(g.edge_count().unwrap(), 1);
500 }
501
502 #[test]
503 fn multi_edge_dependents() {
504 let g = test_graph();
505
506 let a = g.upsert_node(&Node::file("src/a.rs")).unwrap();
507 let b = g.upsert_node(&Node::file("src/b.rs")).unwrap();
508 let c = g.upsert_node(&Node::file("src/c.rs")).unwrap();
509
510 g.upsert_edge(&Edge::new(b, a, EdgeKind::Imports)).unwrap();
511 g.upsert_edge(&Edge::new(c, a, EdgeKind::Calls)).unwrap();
512
513 let deps = g.dependents("src/a.rs").unwrap();
514 assert_eq!(deps.len(), 2);
515 assert!(deps.contains(&"src/b.rs".to_string()));
516 assert!(deps.contains(&"src/c.rs".to_string()));
517 }
518
519 #[test]
520 fn multi_edge_impact_analysis() {
521 let g = test_graph();
522
523 let a = g.upsert_node(&Node::file("a.rs")).unwrap();
524 let b = g.upsert_node(&Node::file("b.rs")).unwrap();
525 let c = g.upsert_node(&Node::file("c.rs")).unwrap();
526
527 g.upsert_edge(&Edge::new(b, a, EdgeKind::Imports)).unwrap();
528 g.upsert_edge(&Edge::new(c, b, EdgeKind::Calls)).unwrap();
529
530 let impact = g.impact_analysis("a.rs", 10).unwrap();
531 assert!(impact.affected_files.contains(&"b.rs".to_string()));
532 assert!(impact.affected_files.contains(&"c.rs".to_string()));
533 }
534
535 #[test]
536 fn related_files_scored() {
537 let g = test_graph();
538
539 let a = g.upsert_node(&Node::file("a.rs")).unwrap();
540 let b = g.upsert_node(&Node::file("b.rs")).unwrap();
541 let c = g.upsert_node(&Node::file("c.rs")).unwrap();
542
543 g.upsert_edge(&Edge::new(a, b, EdgeKind::Imports)).unwrap();
544 g.upsert_edge(&Edge::new(a, b, EdgeKind::Calls)).unwrap();
545 g.upsert_edge(&Edge::new(a, c, EdgeKind::TypeRef)).unwrap();
546
547 let related = g.related_files("a.rs", 10).unwrap();
548 assert_eq!(related.len(), 2);
549 let b_score = related.iter().find(|(p, _)| p == "b.rs").unwrap().1;
550 let c_score = related.iter().find(|(p, _)| p == "c.rs").unwrap().1;
551 assert!(
552 b_score > c_score,
553 "b.rs has imports+calls, should rank higher than c.rs with type_ref"
554 );
555 }
556
557 #[test]
558 fn graph_dir_uses_data_dir_when_set() {
559 let tmp = tempfile::tempdir().unwrap();
560 let project = tmp.path().join("myproject");
561 std::fs::create_dir_all(&project).unwrap();
562
563 let data_dir = tmp.path().join("data");
564 std::fs::create_dir_all(&data_dir).unwrap();
565
566 let _guard = test_env_lock();
567 crate::test_env::set_var("LEAN_CTX_DATA_DIR", data_dir.to_str().unwrap());
568
569 let dir = graph_dir(project.to_str().unwrap());
570 assert!(dir.starts_with(&data_dir));
571 assert!(dir.to_string_lossy().contains("graphs"));
572
573 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
574 }
575
576 #[test]
577 fn graph_dir_returns_consistent_hash_dir() {
578 let tmp = tempfile::tempdir().unwrap();
579 let project = tmp.path().join("hash_project");
580 std::fs::create_dir_all(&project).unwrap();
581
582 let data_dir = tmp.path().join("data2");
583 std::fs::create_dir_all(&data_dir).unwrap();
584
585 let _guard = test_env_lock();
586 crate::test_env::set_var("LEAN_CTX_DATA_DIR", data_dir.to_str().unwrap());
587
588 let dir1 = graph_dir(project.to_str().unwrap());
589 let dir2 = graph_dir(project.to_str().unwrap());
590 assert_eq!(dir1, dir2, "graph_dir should be deterministic");
591 assert!(dir1.to_string_lossy().contains("graphs"));
592
593 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
594 }
595
596 #[test]
597 fn migration_moves_old_files() {
598 let tmp = tempfile::tempdir().unwrap();
599 let project = tmp.path().join("migtest");
600 let old_dir = project.join(".lean-ctx");
601 std::fs::create_dir_all(&old_dir).unwrap();
602 std::fs::write(old_dir.join("graph.db"), b"old-db-content").unwrap();
603 std::fs::write(old_dir.join("graph.meta.json"), b"old-meta").unwrap();
604
605 let new_dir = tmp.path().join("newloc");
606 std::fs::create_dir_all(&new_dir).unwrap();
607
608 migrate_if_needed(project.to_str().unwrap(), &new_dir);
609
610 assert!(new_dir.join("graph.db").exists());
611 assert!(new_dir.join("graph.meta.json").exists());
612 assert!(!old_dir.join("graph.db").exists());
613 assert!(!old_dir.join("graph.meta.json").exists());
614 assert_eq!(
615 std::fs::read_to_string(new_dir.join("graph.db")).unwrap(),
616 "old-db-content"
617 );
618 }
619
620 #[test]
621 fn migration_skips_when_new_exists() {
622 let tmp = tempfile::tempdir().unwrap();
623 let project = tmp.path().join("skiptest");
624 let old_dir = project.join(".lean-ctx");
625 std::fs::create_dir_all(&old_dir).unwrap();
626 std::fs::write(old_dir.join("graph.db"), b"old").unwrap();
627
628 let new_dir = tmp.path().join("newloc2");
629 std::fs::create_dir_all(&new_dir).unwrap();
630 std::fs::write(new_dir.join("graph.db"), b"already-there").unwrap();
631
632 migrate_if_needed(project.to_str().unwrap(), &new_dir);
633
634 assert_eq!(
635 std::fs::read_to_string(new_dir.join("graph.db")).unwrap(),
636 "already-there"
637 );
638 assert!(old_dir.join("graph.db").exists());
639 }
640
641 #[test]
642 fn open_with_data_dir() {
643 let tmp = tempfile::tempdir().unwrap();
644 let project = tmp.path().join("opentest");
645 std::fs::create_dir_all(&project).unwrap();
646
647 let data_dir = tmp.path().join("xdata");
648 std::fs::create_dir_all(&data_dir).unwrap();
649
650 let _guard = test_env_lock();
651 crate::test_env::set_var("LEAN_CTX_DATA_DIR", data_dir.to_str().unwrap());
652
653 let g = CodeGraph::open(project.to_str().unwrap()).unwrap();
654 assert!(g.db_path().starts_with(&data_dir));
655 assert!(g.db_path().to_string_lossy().contains("graph.db"));
656
657 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
658 }
659
660 #[test]
661 fn meta_path_uses_graph_dir() {
662 let tmp = tempfile::tempdir().unwrap();
663 let project = tmp.path().join("metatest");
664 std::fs::create_dir_all(&project).unwrap();
665
666 let data_dir = tmp.path().join("mdata");
667 std::fs::create_dir_all(&data_dir).unwrap();
668
669 let _guard = test_env_lock();
670 crate::test_env::set_var("LEAN_CTX_DATA_DIR", data_dir.to_str().unwrap());
671
672 let mp = meta::meta_path(project.to_str().unwrap());
673 assert!(mp.starts_with(&data_dir));
674 assert!(mp.to_string_lossy().contains("graph.meta.json"));
675
676 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
677 }
678
679 #[test]
680 fn engine_outdated_flags_old_and_missing_meta() {
681 let _iso = crate::core::data_dir::isolated_data_dir();
682 let proj = tempfile::tempdir().unwrap();
683 let root = proj.path().to_str().unwrap();
684
685 assert!(engine_outdated(root), "missing meta must read as outdated");
687
688 let mut meta = PropertyGraphMetaV1 {
690 built_at: "2026-01-01T00:00:00Z".to_string(),
691 engine_version: 0,
692 ..Default::default()
693 };
694 write_meta(root, &meta).unwrap();
695 assert!(
696 engine_outdated(root),
697 "engine_version 0 must read as outdated"
698 );
699
700 meta.engine_version = GRAPH_ENGINE_VERSION;
702 write_meta(root, &meta).unwrap();
703 assert!(
704 !engine_outdated(root),
705 "current engine_version must read as up to date"
706 );
707 }
708}