projectatlas_db/
project_identity.rs

1//! Durable project identity and explicit root-binding transitions.
2
3use super::{AtlasStore, DbError, DbResult, normalize_metadata_path, set_metadata};
4use crate::schema::{self, PROJECT_ROOT_KEY, SchemaState};
5use projectatlas_core::IndexGeneration;
6use projectatlas_core::graph::ProjectInstanceId;
7use rusqlite::{Connection, OptionalExtension};
8use std::fs;
9use std::path::Path;
10
11/// Maximum attempts to obtain a nonzero identity distinct from an existing one.
12const PROJECT_IDENTITY_GENERATION_ATTEMPTS: usize = 8;
13
14/// Explicit root-binding behavior selected by a caller.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ProjectRootTransition {
17    /// Initialize a missing binding or verify an identical existing binding.
18    Bind,
19    /// Preserve identity while moving a database whose previous root is absent.
20    Move,
21    /// Rotate identity for an independent copy, clone, or worktree.
22    Detach,
23}
24
25/// Result of one completed root transition.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct ProjectRootTransitionResult {
28    /// Transition selected by the caller.
29    pub transition: ProjectRootTransition,
30    /// Root stored before the transition, when one existed.
31    pub previous_root: Option<String>,
32    /// Canonical root stored after the transition.
33    pub project_root: String,
34    /// Durable identity owned by the destination after the transition.
35    pub project_instance_id: ProjectInstanceId,
36    /// Whether this operation created or replaced the project identity.
37    pub identity_changed: bool,
38    /// Whether derived publication trust was invalidated.
39    pub publication_invalidated: bool,
40}
41
42impl AtlasStore {
43    /// Apply an explicit root-binding transition to one database path.
44    ///
45    /// `destination` must be an absolute existing project directory and is
46    /// canonicalized before database preflight. `Bind` preserves the old compatible behavior. `Move` preserves identity
47    /// only after the recorded root is proven absent. `Detach` rotates identity
48    /// and discards project-qualified graph rows while preserving authored data.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error for incompatible storage, an implicit rebind, an
53    /// unproven move, a concurrent transition, identity corruption, or any
54    /// transactional `SQLite` failure.
55    pub fn transition_project_root(
56        database_path: &Path,
57        destination: &Path,
58        transition: ProjectRootTransition,
59    ) -> DbResult<ProjectRootTransitionResult> {
60        let destination = validate_project_root_destination(destination)?;
61        let (preflight, _) = schema::preflight(database_path, None)?;
62        let previous_root = preflight.project_root.clone();
63        let previous_identity = if preflight.state == SchemaState::Current {
64            read_current_project_identity(database_path)?
65        } else {
66            None
67        };
68
69        match transition {
70            ProjectRootTransition::Bind => {
71                if let Some(found) = previous_root.as_deref()
72                    && found != destination
73                {
74                    return Err(DbError::ProjectRootMismatch {
75                        expected: destination,
76                        found: found.to_string(),
77                    });
78                }
79                let store = Self::open_for_project(database_path, Path::new(&destination))?;
80                let project_instance_id = store
81                    .project_instance_id()?
82                    .ok_or(DbError::ProjectInstanceIdentityMissing)?;
83                Ok(ProjectRootTransitionResult {
84                    transition,
85                    previous_root,
86                    project_root: destination,
87                    project_instance_id,
88                    identity_changed: previous_identity != Some(project_instance_id),
89                    publication_invalidated: false,
90                })
91            }
92            ProjectRootTransition::Move | ProjectRootTransition::Detach => {
93                let previous_root =
94                    previous_root.ok_or(DbError::ProjectRootTransitionRequiresExistingRoot)?;
95                if transition == ProjectRootTransition::Move {
96                    if previous_root == destination {
97                        return Err(DbError::ProjectRootTransitionRequiresDifferentRoot {
98                            root: destination,
99                        });
100                    }
101                    verify_root_absent(&previous_root)?;
102                }
103
104                let mut store = Self::open_for_root_transition(database_path)?;
105                let opened_identity = store.project_instance_id()?;
106                if previous_identity.is_some() && opened_identity != previous_identity {
107                    return Err(project_transition_changed(
108                        Some(previous_root),
109                        store.project_root()?,
110                        previous_identity,
111                        opened_identity,
112                    ));
113                }
114                let mut result = apply_root_transition(
115                    &mut store,
116                    transition,
117                    &previous_root,
118                    opened_identity,
119                    &destination,
120                )?;
121                result.identity_changed = previous_identity != Some(result.project_instance_id);
122                Ok(result)
123            }
124        }
125    }
126
127    /// Return the durable project instance identity, when initialized.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error when the singleton row is malformed or cannot be read.
132    pub fn project_instance_id(&self) -> DbResult<Option<ProjectInstanceId>> {
133        load_project_identity(&self.connection)
134    }
135}
136
137/// Validate and canonicalize a transition destination before touching its database.
138fn validate_project_root_destination(destination: &Path) -> DbResult<String> {
139    let root = normalize_metadata_path(destination);
140    if !destination.is_absolute() {
141        return Err(DbError::ProjectRootDestinationInvalid {
142            root,
143            source: std::io::Error::new(
144                std::io::ErrorKind::InvalidInput,
145                "project root destination is not absolute",
146            ),
147        });
148    }
149    let canonical =
150        fs::canonicalize(destination).map_err(|source| DbError::ProjectRootDestinationInvalid {
151            root: root.clone(),
152            source,
153        })?;
154    let metadata =
155        fs::metadata(&canonical).map_err(|source| DbError::ProjectRootDestinationInvalid {
156            root: root.clone(),
157            source,
158        })?;
159    if !metadata.is_dir() {
160        return Err(DbError::ProjectRootDestinationInvalid {
161            root,
162            source: std::io::Error::new(
163                std::io::ErrorKind::InvalidInput,
164                "project root destination is not a directory",
165            ),
166        });
167    }
168    Ok(normalize_metadata_path(&canonical))
169}
170
171/// Apply move or detach after non-mutating preflight has captured expected state.
172fn apply_root_transition(
173    store: &mut AtlasStore,
174    transition: ProjectRootTransition,
175    expected_root: &str,
176    expected_identity: Option<ProjectInstanceId>,
177    destination: &str,
178) -> DbResult<ProjectRootTransitionResult> {
179    store.connection.execute_batch("BEGIN IMMEDIATE")?;
180    let operation = (|| {
181        let found_root = store.project_root()?;
182        let found_identity = load_project_identity(&store.connection)?;
183        if found_root.as_deref() != Some(expected_root) || found_identity != expected_identity {
184            return Err(project_transition_changed(
185                Some(expected_root.to_string()),
186                found_root,
187                expected_identity,
188                found_identity,
189            ));
190        }
191
192        if transition == ProjectRootTransition::Detach
193            && let Some(previous_identity) = found_identity
194        {
195            crate::telemetry::seal_project_usage_instances(&store.connection, previous_identity)?;
196        }
197        set_metadata(&store.connection, PROJECT_ROOT_KEY, destination)?;
198        schema::invalidate_derived_publication(&store.connection)?;
199        let (project_instance_id, identity_changed) = match transition {
200            ProjectRootTransition::Bind => unreachable!("bind does not use transition mutation"),
201            ProjectRootTransition::Move => {
202                let (identity, identity_changed) = ensure_project_identity(&store.connection)?;
203                set_graph_generation(&store.connection, IndexGeneration::ZERO)?;
204                (identity, identity_changed)
205            }
206            ProjectRootTransition::Detach => {
207                store
208                    .connection
209                    .execute("DELETE FROM graph_resolution_keys", [])?;
210                store.connection.execute("DELETE FROM graph_coverage", [])?;
211                store
212                    .connection
213                    .execute("DELETE FROM graph_relations", [])?;
214                store.connection.execute("DELETE FROM graph_entities", [])?;
215                let identity = generate_project_identity(&store.connection, found_identity)?;
216                set_project_identity(&store.connection, identity)?;
217                (identity, true)
218            }
219        };
220
221        Ok(ProjectRootTransitionResult {
222            transition,
223            previous_root: Some(expected_root.to_string()),
224            project_root: destination.to_string(),
225            project_instance_id,
226            identity_changed,
227            publication_invalidated: true,
228        })
229    })();
230    match operation {
231        Ok(result) => {
232            if let Err(source) = store.connection.execute_batch("COMMIT") {
233                return Err(schema::rollback_after_error(
234                    &store.connection,
235                    DbError::Sqlite(source),
236                ));
237            }
238            store.validated_project_root = Some(destination.to_string());
239            store.validated_project_instance_id = Some(result.project_instance_id);
240            if result.identity_changed {
241                store.library_usage_instances.get_mut().clear();
242            }
243            Ok(result)
244        }
245        Err(error) => Err(schema::rollback_after_error(&store.connection, error)),
246    }
247}
248
249/// Prove the recorded old root path entry is absent without following links.
250fn verify_root_absent(root: &str) -> DbResult<()> {
251    let path = Path::new(root);
252    if !path.is_absolute() {
253        return Err(DbError::ProjectRootAbsenceUncertain {
254            root: root.to_string(),
255            source: std::io::Error::new(
256                std::io::ErrorKind::InvalidInput,
257                "stored project root is not absolute",
258            ),
259        });
260    }
261    classify_root_absence(root, fs::symlink_metadata(path).map(|_| ()))
262}
263
264/// Classify a non-following filesystem probe without weakening uncertain failures.
265fn classify_root_absence(root: &str, result: std::io::Result<()>) -> DbResult<()> {
266    match result {
267        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
268        Ok(()) => Err(DbError::ProjectRootStillPresent {
269            root: root.to_string(),
270        }),
271        Err(source) => Err(DbError::ProjectRootAbsenceUncertain {
272            root: root.to_string(),
273            source,
274        }),
275    }
276}
277
278/// Load identity through a validated current read-only snapshot.
279fn read_current_project_identity(path: &Path) -> DbResult<Option<ProjectInstanceId>> {
280    let (connection, _) = schema::open_current_read_only(path, None)?;
281    load_project_identity(&connection)
282}
283
284/// Read and validate the project singleton identity.
285pub(crate) fn load_project_identity(
286    connection: &Connection,
287) -> DbResult<Option<ProjectInstanceId>> {
288    let bytes = connection
289        .query_row(
290            "SELECT project_instance_id FROM project_identity WHERE singleton = 1",
291            [],
292            |row| row.get::<_, Vec<u8>>(0),
293        )
294        .optional()?;
295    bytes.map(project_identity_from_blob).transpose()
296}
297
298/// Read the typed graph generation owned by the project singleton.
299pub(crate) fn load_graph_generation(connection: &Connection) -> DbResult<Option<IndexGeneration>> {
300    let generation = connection
301        .query_row(
302            "SELECT active_generation FROM project_identity WHERE singleton = 1",
303            [],
304            |row| row.get::<_, i64>(0),
305        )
306        .optional()?;
307    generation
308        .map(|value| {
309            let value = u64::try_from(value).map_err(|source| DbError::InvalidCount {
310                field: "project_identity.active_generation",
311                value,
312                source,
313            })?;
314            Ok(IndexGeneration::new(value))
315        })
316        .transpose()
317}
318
319/// Return whether the project singleton exists and matches the selected identity.
320pub(crate) fn verify_project_identity(
321    connection: &Connection,
322    expected: ProjectInstanceId,
323) -> DbResult<bool> {
324    let Some(found) = load_project_identity(connection)? else {
325        return Ok(false);
326    };
327    require_project_identity(expected, found)?;
328    Ok(true)
329}
330
331/// Require the already-bound destination identity for graph publication.
332pub(crate) fn require_bound_project_identity(
333    connection: &Connection,
334    expected: ProjectInstanceId,
335) -> DbResult<()> {
336    let found =
337        load_project_identity(connection)?.ok_or(DbError::ProjectInstanceIdentityMissing)?;
338    require_project_identity(expected, found)
339}
340
341/// Create a project identity when a bound database has not initialized one yet.
342pub(crate) fn ensure_project_identity(
343    connection: &Connection,
344) -> DbResult<(ProjectInstanceId, bool)> {
345    if let Some(identity) = load_project_identity(connection)? {
346        return Ok((identity, false));
347    }
348    let identity = generate_project_identity(connection, None)?;
349    set_project_identity(connection, identity)?;
350    Ok((identity, true))
351}
352
353/// Set the graph generation after a validated publication or invalidation.
354pub(crate) fn set_graph_generation(
355    connection: &Connection,
356    generation: IndexGeneration,
357) -> DbResult<()> {
358    let generation =
359        i64::try_from(generation.get()).map_err(|_source| DbError::GraphCountOverflow {
360            field: "project_identity.active_generation",
361            value: generation.get(),
362        })?;
363    connection.execute(
364        "UPDATE project_identity SET active_generation = ?1 WHERE singleton = 1",
365        [generation],
366    )?;
367    Ok(())
368}
369
370/// Generate a nonzero SQLite-owned identity distinct from an optional predecessor.
371fn generate_project_identity(
372    connection: &Connection,
373    predecessor: Option<ProjectInstanceId>,
374) -> DbResult<ProjectInstanceId> {
375    for _ in 0..PROJECT_IDENTITY_GENERATION_ATTEMPTS {
376        let bytes =
377            connection.query_row("SELECT randomblob(16)", [], |row| row.get::<_, Vec<u8>>(0))?;
378        if let Ok(identity) = project_identity_from_blob(bytes)
379            && Some(identity) != predecessor
380        {
381            return Ok(identity);
382        }
383    }
384    Err(DbError::ProjectInstanceIdentityGenerationFailed)
385}
386
387/// Insert or replace the singleton identity after dependent graph rows are removed.
388pub(crate) fn set_project_identity(
389    connection: &Connection,
390    identity: ProjectInstanceId,
391) -> DbResult<()> {
392    connection.execute(
393        "INSERT INTO project_identity(singleton, project_instance_id, active_generation)
394         VALUES(1, ?1, 0)
395         ON CONFLICT(singleton) DO UPDATE SET
396            project_instance_id = excluded.project_instance_id,
397            active_generation = 0",
398        [&identity.as_bytes()[..]],
399    )?;
400    Ok(())
401}
402
403/// Convert the fixed persisted identity representation into its domain newtype.
404fn project_identity_from_blob(value: Vec<u8>) -> DbResult<ProjectInstanceId> {
405    let found = value.len();
406    let bytes: [u8; 16] = value
407        .try_into()
408        .map_err(|_value| DbError::InvalidBlobLength {
409            field: "project_identity.project_instance_id",
410            expected: 16,
411            found,
412        })?;
413    ProjectInstanceId::from_bytes(bytes).map_err(Into::into)
414}
415
416/// Require identical project ownership with a typed mismatch diagnostic.
417fn require_project_identity(expected: ProjectInstanceId, found: ProjectInstanceId) -> DbResult<()> {
418    if expected != found {
419        return Err(DbError::GraphProjectIdentityMismatch {
420            expected: expected.to_string(),
421            found: found.to_string(),
422        });
423    }
424    Ok(())
425}
426
427/// Build a typed concurrent-transition failure with both captured states.
428fn project_transition_changed(
429    expected_root: Option<String>,
430    found_root: Option<String>,
431    expected_identity: Option<ProjectInstanceId>,
432    found_identity: Option<ProjectInstanceId>,
433) -> DbError {
434    DbError::ProjectRootTransitionChanged {
435        expected_root,
436        found_root,
437        expected_identity: expected_identity.map(|identity| identity.to_string()),
438        found_identity: found_identity.map(|identity| identity.to_string()),
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::HealthResolution;
446    use projectatlas_core::graph::{
447        CanonicalResolutionKey, Completeness, ConfidenceClass, CoverageRecord, CoverageScope,
448        CoverageState, EntityResolutionKey, EntitySelector, GraphEntity, GraphIdentityText,
449        GraphRelationKind, LogicalRelation, RelationDependencyKey, RelationOccurrence,
450        RelationResolution, RepositoryFilePath, ResolutionKeyDomain, SourceSpan,
451    };
452    use projectatlas_core::symbols::RelationKind;
453    use projectatlas_core::telemetry::usage_from_estimates;
454    use std::error::Error;
455    use std::fmt::Debug;
456    use std::io;
457
458    #[test]
459    fn root_transitions_preserve_authored_state_and_isolate_copies() -> Result<(), Box<dyn Error>> {
460        let temp = tempfile::tempdir()?;
461        let root_a = temp.path().join("source-Δ");
462        let root_b = temp.path().join("detached-copy");
463        let root_c = temp.path().join("moved-source");
464        fs::create_dir_all(&root_a)?;
465        fs::create_dir_all(&root_b)?;
466        fs::create_dir_all(&root_c)?;
467        let source_db = temp.path().join("source.db");
468        let copy_db = temp.path().join("copy.db");
469        let rollback_db = temp.path().join("rollback.db");
470
471        let bound =
472            AtlasStore::transition_project_root(&source_db, &root_a, ProjectRootTransition::Bind)?;
473        require(
474            bound.previous_root.is_none(),
475            "fresh bind had a previous root",
476        )?;
477        require(bound.identity_changed, "fresh bind did not create identity")?;
478        require(
479            !bound.publication_invalidated,
480            "fresh bind invalidated publication",
481        )?;
482        let source_identity = bound.project_instance_id;
483        let rebound =
484            AtlasStore::transition_project_root(&source_db, &root_a, ProjectRootTransition::Bind)?;
485        require_eq(
486            &rebound.project_instance_id,
487            &source_identity,
488            "same-root bind identity",
489        )?;
490        require(!rebound.identity_changed, "same-root bind changed identity")?;
491
492        let mut source = AtlasStore::open_for_project(&source_db, &root_a)?;
493        seed_authored_and_graph_state(&mut source, source_identity)?;
494        let foreign_identity = ProjectInstanceId::from_bytes([0x7a; 16])?;
495        let foreign_project = GraphEntity::new(
496            foreign_identity,
497            EntitySelector::Project,
498            IndexGeneration::new(2),
499        )?;
500        let mut rejected_publication = source.begin_index_publication("foreign-project")?;
501        let foreign_error = require_error(
502            rejected_publication.replace_repository_graph(
503                foreign_identity,
504                &[foreign_project],
505                &[],
506                &[],
507                &[],
508            ),
509            "foreign graph publication replaced destination identity",
510        )?;
511        require(
512            matches!(foreign_error, DbError::GraphProjectIdentityMismatch { .. }),
513            "foreign graph publication returned the wrong error",
514        )?;
515        drop(rejected_publication);
516        source
517            .connection
518            .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
519        drop(source);
520        fs::copy(&source_db, &copy_db)?;
521        fs::copy(&source_db, &rollback_db)?;
522        let copied_bytes = fs::read(&copy_db)?;
523
524        let invalid_file_root = temp.path().join("not-a-project-directory");
525        let missing_root = temp.path().join("missing-project-root");
526        fs::write(&invalid_file_root, "not a directory")?;
527        for (destination, label) in [
528            (Path::new("relative-project-root"), "relative destination"),
529            (missing_root.as_path(), "missing destination"),
530            (invalid_file_root.as_path(), "file destination"),
531        ] {
532            let invalid_error = require_error(
533                AtlasStore::transition_project_root(
534                    &copy_db,
535                    destination,
536                    ProjectRootTransition::Detach,
537                ),
538                "invalid transition destination was accepted",
539            )?;
540            require(
541                matches!(invalid_error, DbError::ProjectRootDestinationInvalid { .. }),
542                "invalid destination returned the wrong error",
543            )?;
544            assert_database_unchanged(&copy_db, &copied_bytes, label)?;
545        }
546
547        let bind_error = require_error(
548            AtlasStore::transition_project_root(&copy_db, &root_b, ProjectRootTransition::Bind),
549            "copied database accepted an implicit rebind",
550        )?;
551        require(
552            matches!(bind_error, DbError::ProjectRootMismatch { .. }),
553            "copied database bind returned the wrong error",
554        )?;
555        assert_database_unchanged(&copy_db, &copied_bytes, "rejected bind database")?;
556        let move_error = require_error(
557            AtlasStore::transition_project_root(&copy_db, &root_b, ProjectRootTransition::Move),
558            "copy preserved identity while original root was accessible",
559        )?;
560        require(
561            matches!(move_error, DbError::ProjectRootStillPresent { .. }),
562            "accessible-root move returned the wrong error",
563        )?;
564        assert_database_unchanged(&copy_db, &copied_bytes, "rejected move database")?;
565        let rejected_store = AtlasStore::open_read_only_for_project(&copy_db, &root_a)?;
566        require_eq(
567            &rejected_store.project_instance_id()?,
568            &Some(source_identity),
569            "rejected transition identity",
570        )?;
571        assert_authored_state(&rejected_store)?;
572        assert_usage_report(&rejected_store, true)?;
573        assert_runtime_scope(&rejected_store, source_identity, 1, 0, 1)?;
574        assert_graph_counts(&rejected_store, [2, 1, 1, 1, 1, 1, 1])?;
575        require(
576            rejected_store.index_publication()?.is_some(),
577            "rejected transition invalidated publication",
578        )?;
579        drop(rejected_store);
580
581        let regular_root = temp.path().join("former-root-now-file");
582        let regular_db = temp.path().join("regular-root.db");
583        fs::create_dir(&regular_root)?;
584        AtlasStore::transition_project_root(
585            &regular_db,
586            &regular_root,
587            ProjectRootTransition::Bind,
588        )?;
589        fs::remove_dir(&regular_root)?;
590        fs::write(&regular_root, "the old root path still exists")?;
591        let regular_bytes = fs::read(&regular_db)?;
592        let regular_error = require_error(
593            AtlasStore::transition_project_root(&regular_db, &root_c, ProjectRootTransition::Move),
594            "regular file at the old root was treated as absent",
595        )?;
596        require(
597            matches!(regular_error, DbError::ProjectRootStillPresent { .. }),
598            "regular-file move returned the wrong error",
599        )?;
600        assert_database_unchanged(&regular_db, &regular_bytes, "regular-file move database")?;
601
602        let link_root = temp.path().join("former-root-now-link");
603        let missing_link_target = temp.path().join("missing-link-target");
604        let link_db = temp.path().join("linked-root.db");
605        fs::create_dir(&link_root)?;
606        AtlasStore::transition_project_root(&link_db, &link_root, ProjectRootTransition::Bind)?;
607        fs::remove_dir(&link_root)?;
608        if create_dangling_directory_link(&missing_link_target, &link_root)? {
609            let link_bytes = fs::read(&link_db)?;
610            let link_error = require_error(
611                AtlasStore::transition_project_root(&link_db, &root_c, ProjectRootTransition::Move),
612                "dangling root link was treated as absent",
613            )?;
614            require(
615                matches!(link_error, DbError::ProjectRootStillPresent { .. }),
616                "dangling-link move returned the wrong error",
617            )?;
618            assert_database_unchanged(&link_db, &link_bytes, "dangling-link move database")?;
619            fs::remove_file(&link_root)?;
620        }
621
622        let relative_root = temp.path().join("relative-root-source");
623        let relative_db = temp.path().join("relative-root.db");
624        fs::create_dir(&relative_root)?;
625        AtlasStore::transition_project_root(
626            &relative_db,
627            &relative_root,
628            ProjectRootTransition::Bind,
629        )?;
630        {
631            let relative_store = AtlasStore::open(&relative_db)?;
632            set_metadata(
633                &relative_store.connection,
634                PROJECT_ROOT_KEY,
635                "relative/stored/root",
636            )?;
637            relative_store
638                .connection
639                .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
640        }
641        let relative_bytes = fs::read(&relative_db)?;
642        let relative_error = require_error(
643            AtlasStore::transition_project_root(&relative_db, &root_c, ProjectRootTransition::Move),
644            "non-absolute stored root was treated as a verified move",
645        )?;
646        require(
647            matches!(relative_error, DbError::ProjectRootAbsenceUncertain { .. }),
648            "non-absolute stored root returned the wrong error",
649        )?;
650        assert_database_unchanged(&relative_db, &relative_bytes, "relative-root move database")?;
651
652        let uncertain_error = require_error(
653            classify_root_absence(
654                "C:/uncertain-project-root",
655                Err(std::io::Error::new(
656                    std::io::ErrorKind::PermissionDenied,
657                    "injected permission denial",
658                )),
659            ),
660            "permission uncertainty was treated as absence",
661        )?;
662        require(
663            matches!(uncertain_error, DbError::ProjectRootAbsenceUncertain { .. }),
664            "permission uncertainty returned the wrong error",
665        )?;
666
667        let legacy_db = temp.path().join("legacy-root.db");
668        let legacy_old_root = temp.path().join("legacy-old-root");
669        let legacy_destination = temp.path().join("legacy-destination");
670        fs::create_dir(&legacy_destination)?;
671        {
672            let legacy = Connection::open(&legacy_db)?;
673            schema::create_released_schema_eight(&legacy)?;
674            set_metadata(
675                &legacy,
676                PROJECT_ROOT_KEY,
677                &normalize_metadata_path(&legacy_old_root),
678            )?;
679        }
680        let migrated_move = AtlasStore::transition_project_root(
681            &legacy_db,
682            &legacy_destination,
683            ProjectRootTransition::Move,
684        )?;
685        require(
686            migrated_move.identity_changed,
687            "pre-graph-schema move did not report its initialized identity",
688        )?;
689        require(
690            migrated_move.project_instance_id.as_bytes() != [0_u8; 16],
691            "pre-graph-schema move initialized a zero identity",
692        )?;
693
694        let current_missing_identity_db = temp.path().join("current-missing-identity.db");
695        let current_missing_identity_root = temp.path().join("current-missing-identity-root");
696        let current_missing_identity_destination =
697            temp.path().join("current-missing-identity-destination");
698        fs::create_dir(&current_missing_identity_root)?;
699        fs::create_dir(&current_missing_identity_destination)?;
700        AtlasStore::transition_project_root(
701            &current_missing_identity_db,
702            &current_missing_identity_root,
703            ProjectRootTransition::Bind,
704        )?;
705        {
706            let current = AtlasStore::open(&current_missing_identity_db)?;
707            current
708                .connection
709                .execute("DELETE FROM project_identity", [])?;
710            current
711                .connection
712                .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
713        }
714        fs::remove_dir(&current_missing_identity_root)?;
715        let repaired_current_move = AtlasStore::transition_project_root(
716            &current_missing_identity_db,
717            &current_missing_identity_destination,
718            ProjectRootTransition::Move,
719        )?;
720        require(
721            repaired_current_move.identity_changed,
722            "current bound database move did not report its repaired identity",
723        )?;
724        let repaired_current = AtlasStore::open_read_only_for_project(
725            &current_missing_identity_db,
726            &current_missing_identity_destination,
727        )?;
728        require_eq(
729            &repaired_current.project_instance_id()?,
730            &Some(repaired_current_move.project_instance_id),
731            "current bound database repaired identity",
732        )?;
733
734        let detached =
735            AtlasStore::transition_project_root(&copy_db, &root_b, ProjectRootTransition::Detach)?;
736        require(
737            detached.project_instance_id != source_identity,
738            "detach preserved copied identity",
739        )?;
740        require(detached.identity_changed, "detach did not change identity")?;
741        require(
742            detached.publication_invalidated,
743            "detach did not invalidate publication",
744        )?;
745        let detached_store = AtlasStore::open_read_only_for_project(&copy_db, &root_b)?;
746        require_eq(
747            &detached_store.project_instance_id()?,
748            &Some(detached.project_instance_id),
749            "detached identity",
750        )?;
751        assert_authored_state(&detached_store)?;
752        assert_usage_report(&detached_store, false)?;
753        assert_runtime_scope(&detached_store, source_identity, 0, 1, 0)?;
754        assert_graph_counts(&detached_store, [0, 0, 0, 0, 0, 0, 0])?;
755        require(
756            detached_store.index_publication()?.is_none(),
757            "detach retained publication",
758        )?;
759
760        let source_store = AtlasStore::open_read_only_for_project(&source_db, &root_a)?;
761        require_eq(
762            &source_store.project_instance_id()?,
763            &Some(source_identity),
764            "source identity after copy detach",
765        )?;
766        assert_authored_state(&source_store)?;
767        assert_usage_report(&source_store, true)?;
768        assert_runtime_scope(&source_store, source_identity, 1, 0, 1)?;
769        assert_graph_counts(&source_store, [2, 1, 1, 1, 1, 1, 1])?;
770        drop(source_store);
771
772        let mut rollback_store = AtlasStore::open(&rollback_db)?;
773        rollback_store.connection.execute_batch(
774            "CREATE TEMP TRIGGER fail_detach_graph
775             BEFORE DELETE ON graph_entities
776             BEGIN SELECT RAISE(ABORT, 'injected detach failure'); END;",
777        )?;
778        let rollback_error = require_error(
779            apply_root_transition(
780                &mut rollback_store,
781                ProjectRootTransition::Detach,
782                &normalize_metadata_path(&root_a),
783                Some(source_identity),
784                &normalize_metadata_path(&root_b),
785            ),
786            "late detach failure committed partial identity state",
787        )?;
788        require(
789            matches!(rollback_error, DbError::Sqlite(_)),
790            "late detach failure returned the wrong error",
791        )?;
792        require_eq(
793            &rollback_store.project_root()?,
794            &Some(normalize_metadata_path(&root_a)),
795            "rollback project root",
796        )?;
797        require_eq(
798            &rollback_store.project_instance_id()?,
799            &Some(source_identity),
800            "rollback project identity",
801        )?;
802        assert_authored_state(&rollback_store)?;
803        assert_usage_report(&rollback_store, true)?;
804        assert_runtime_scope(&rollback_store, source_identity, 1, 0, 1)?;
805        assert_graph_counts(&rollback_store, [2, 1, 1, 1, 1, 1, 1])?;
806        require(
807            rollback_store.index_publication()?.is_some(),
808            "rollback invalidated prior publication",
809        )?;
810        drop(rollback_store);
811
812        fs::remove_dir(&root_a)?;
813        let moved =
814            AtlasStore::transition_project_root(&source_db, &root_c, ProjectRootTransition::Move)?;
815        require_eq(
816            &moved.project_instance_id,
817            &source_identity,
818            "moved identity",
819        )?;
820        require(!moved.identity_changed, "move changed identity")?;
821        require(
822            moved.publication_invalidated,
823            "move did not invalidate publication",
824        )?;
825        let moved_store = AtlasStore::open_read_only_for_project(&source_db, &root_c)?;
826        require_eq(
827            &moved_store.project_instance_id()?,
828            &Some(source_identity),
829            "stored moved identity",
830        )?;
831        assert_authored_state(&moved_store)?;
832        assert_usage_report(&moved_store, true)?;
833        assert_runtime_scope(&moved_store, source_identity, 1, 0, 1)?;
834        assert_graph_counts(&moved_store, [2, 1, 1, 1, 1, 1, 1])?;
835        require(
836            moved_store.index_publication()?.is_none(),
837            "move retained publication",
838        )?;
839        let active_generation = moved_store.connection.query_row(
840            "SELECT active_generation FROM project_identity WHERE singleton = 1",
841            [],
842            |row| row.get::<_, i64>(0),
843        )?;
844        require_eq(&active_generation, &0, "moved graph generation")?;
845        Ok(())
846    }
847
848    #[test]
849    fn stale_stores_cannot_write_after_binding_transitions() -> Result<(), Box<dyn Error>> {
850        let temp = tempfile::tempdir()?;
851        let root = temp.path().join("same-root");
852        fs::create_dir(&root)?;
853        let database = temp.path().join("same-root.db");
854        let initial =
855            AtlasStore::transition_project_root(&database, &root, ProjectRootTransition::Bind)?;
856        let mut stale = AtlasStore::open_for_project(&database, &root)?;
857        seed_authored_and_graph_state(&mut stale, initial.project_instance_id)?;
858
859        let detached =
860            AtlasStore::transition_project_root(&database, &root, ProjectRootTransition::Detach)?;
861        require(
862            detached.project_instance_id != initial.project_instance_id,
863            "same-root detach did not rotate identity",
864        )?;
865        let purpose_error = require_error(
866            stale.set_purpose(
867                "src/lib.rs",
868                "Stale store must not replace this purpose.",
869                projectatlas_core::PurposeSource::Agent,
870            ),
871            "stale store wrote purpose after same-root detach",
872        )?;
873        require(
874            matches!(purpose_error, DbError::ProjectRootTransitionChanged { .. }),
875            "same-root stale purpose returned the wrong error",
876        )?;
877        let scan_error = require_error(
878            stale.replace_scan(&[]),
879            "stale store replaced scan state after same-root detach",
880        )?;
881        require(
882            matches!(scan_error, DbError::ProjectRootTransitionChanged { .. }),
883            "same-root stale scan returned the wrong error",
884        )?;
885        let telemetry_error = require_error(
886            stale.record_usage(&usage_from_estimates(
887                "stale-after-detach",
888                "summary",
889                Some("src/lib.rs".to_string()),
890                None,
891                100,
892                20,
893            )),
894            "stale store recorded telemetry after same-root detach",
895        )?;
896        require(
897            matches!(
898                telemetry_error,
899                DbError::ProjectRootTransitionChanged { .. }
900            ),
901            "same-root stale telemetry returned the wrong error",
902        )?;
903        let health_error = require_error(
904            stale.resolve_health_finding(&HealthResolution {
905                finding_id: "stale-resolution".to_string(),
906                category: "missing-purpose".to_string(),
907                path: "src/lib.rs".to_string(),
908                related_path: None,
909                rationale: "A stale store must not persist this resolution.".to_string(),
910            }),
911            "stale store resolved health state after same-root detach",
912        )?;
913        require(
914            matches!(health_error, DbError::ProjectRootTransitionChanged { .. }),
915            "same-root stale health resolution returned the wrong error",
916        )?;
917        let publication_error = require_error(
918            stale.begin_index_publication("stale-after-detach"),
919            "stale store began publication after same-root detach",
920        )?;
921        require(
922            matches!(
923                publication_error,
924                DbError::ProjectRootTransitionChanged { .. }
925            ),
926            "same-root stale publication returned the wrong error",
927        )?;
928        let detached_store = AtlasStore::open_read_only_for_project(&database, &root)?;
929        assert_authored_state(&detached_store)?;
930        assert_usage_report(&detached_store, false)?;
931        assert_runtime_scope(&detached_store, initial.project_instance_id, 0, 1, 0)?;
932        assert_graph_counts(&detached_store, [0, 0, 0, 0, 0, 0, 0])?;
933        require(
934            detached_store.index_publication()?.is_none(),
935            "stale writes restored invalidated publication state",
936        )?;
937        let active_generation = detached_store.connection.query_row(
938            "SELECT active_generation FROM project_identity WHERE singleton = 1",
939            [],
940            |row| row.get::<_, i64>(0),
941        )?;
942        require_eq(
943            &active_generation,
944            &0,
945            "active generation after rejected stale writes",
946        )?;
947        drop(detached_store);
948        drop(stale);
949
950        let move_root = temp.path().join("move-source");
951        let destination = temp.path().join("move-destination");
952        fs::create_dir(&move_root)?;
953        fs::create_dir(&destination)?;
954        let move_database = temp.path().join("move.db");
955        AtlasStore::transition_project_root(
956            &move_database,
957            &move_root,
958            ProjectRootTransition::Bind,
959        )?;
960        let mut stale_move = AtlasStore::open_for_project(&move_database, &move_root)?;
961        fs::remove_dir(&move_root)?;
962        AtlasStore::transition_project_root(
963            &move_database,
964            &destination,
965            ProjectRootTransition::Move,
966        )?;
967        let move_error = require_error(
968            stale_move.begin_index_publication("stale-after-move"),
969            "stale store began publication after root move",
970        )?;
971        require(
972            matches!(move_error, DbError::ProjectRootMismatch { .. }),
973            "moved stale publication returned the wrong error",
974        )?;
975        drop(AtlasStore::open_read_only_for_project(
976            &move_database,
977            &destination,
978        )?);
979        Ok(())
980    }
981
982    /// Require a test condition without panicking in a fallible test.
983    fn require(condition: bool, message: &str) -> Result<(), Box<dyn Error>> {
984        if condition {
985            Ok(())
986        } else {
987            Err(io::Error::other(message).into())
988        }
989    }
990
991    /// Require equality while retaining useful mismatch context.
992    fn require_eq<T: Debug + PartialEq>(
993        actual: &T,
994        expected: &T,
995        label: &str,
996    ) -> Result<(), Box<dyn Error>> {
997        if actual == expected {
998            Ok(())
999        } else {
1000            Err(io::Error::other(format!(
1001                "{label} mismatch: expected {expected:?}, got {actual:?}"
1002            ))
1003            .into())
1004        }
1005    }
1006
1007    /// Require one database operation to fail without panic-based assertions.
1008    fn require_error<T>(result: DbResult<T>, message: &str) -> Result<DbError, Box<dyn Error>> {
1009        match result {
1010            Ok(_) => Err(io::Error::other(message).into()),
1011            Err(error) => Ok(error),
1012        }
1013    }
1014
1015    /// Create a dangling directory link when the current platform permits it.
1016    #[cfg(unix)]
1017    fn create_dangling_directory_link(target: &Path, link: &Path) -> Result<bool, Box<dyn Error>> {
1018        std::os::unix::fs::symlink(target, link)?;
1019        Ok(true)
1020    }
1021
1022    /// Create a dangling directory reparse point when Windows policy permits it.
1023    #[cfg(windows)]
1024    fn create_dangling_directory_link(target: &Path, link: &Path) -> Result<bool, Box<dyn Error>> {
1025        match std::os::windows::fs::symlink_dir(target, link) {
1026            Ok(()) => Ok(true),
1027            Err(error)
1028                if error.kind() == std::io::ErrorKind::PermissionDenied
1029                    || error.raw_os_error() == Some(1314) =>
1030            {
1031                Ok(false)
1032            }
1033            Err(error) => Err(error.into()),
1034        }
1035    }
1036
1037    /// Other targets have no standard-library directory-link fixture.
1038    #[cfg(not(any(unix, windows)))]
1039    fn create_dangling_directory_link(
1040        _target: &Path,
1041        _link: &Path,
1042    ) -> Result<bool, Box<dyn Error>> {
1043        Ok(false)
1044    }
1045
1046    fn seed_authored_and_graph_state(
1047        store: &mut AtlasStore,
1048        project: ProjectInstanceId,
1049    ) -> Result<(), Box<dyn Error>> {
1050        store.connection.execute_batch(
1051            "INSERT INTO nodes(path, kind, parent_path) VALUES('.', 'folder', NULL);
1052             INSERT INTO nodes(path, kind, parent_path) VALUES('src/lib.rs', 'file', '.');
1053             INSERT INTO purposes(node_id, purpose, source, status, updated_by)
1054                SELECT id, 'Own the local source entry point.', 'agent', 'approved', 'agent'
1055                  FROM nodes WHERE path = 'src/lib.rs';
1056             INSERT INTO metadata(key, value) VALUES('custom.identity-test-setting', 'retain-me');
1057             INSERT INTO health_resolutions(
1058                finding_id, category, path, related_path, rationale, resolved_by
1059             ) VALUES(
1060                'resolved-purpose', 'duplicate-purpose', 'src/lib.rs', 'src/main.rs',
1061                'The responsibilities are intentionally distinct.', 'agent'
1062             );",
1063        )?;
1064        store.record_usage(&identity_transition_usage_event())?;
1065        let generation = IndexGeneration::new(1);
1066        let project_entity = GraphEntity::new(project, EntitySelector::Project, generation)?;
1067        let file_entity = GraphEntity::new(
1068            project,
1069            EntitySelector::File {
1070                path: RepositoryFilePath::new(Path::new("src/lib.rs"))?,
1071            },
1072            generation,
1073        )?;
1074        let relation = LogicalRelation::new(
1075            &file_entity,
1076            GraphRelationKind::Legacy(RelationKind::DependsOn),
1077            RelationResolution::Unresolved {
1078                reference: GraphIdentityText::new("src/lib.rs")?,
1079            },
1080            ConfidenceClass::Exact,
1081            Completeness::Complete,
1082            generation,
1083        )?;
1084        let occurrence = RelationOccurrence::new(
1085            &relation,
1086            RepositoryFilePath::new(Path::new("src/lib.rs"))?,
1087            SourceSpan::new(1, 0, 1, 1)?,
1088            generation,
1089        )?;
1090        let coverage = CoverageRecord::new(
1091            CoverageScope::Project,
1092            None,
1093            CoverageState::Complete,
1094            1,
1095            0,
1096            generation,
1097            None,
1098            None,
1099        )?;
1100        let resolution_key = CanonicalResolutionKey::new(
1101            project,
1102            ResolutionKeyDomain::Declaration,
1103            &GraphIdentityText::new("identity-transition")?,
1104            &GraphIdentityText::new("rust")?,
1105            None,
1106            None,
1107            Some(GraphRelationKind::Legacy(RelationKind::DependsOn)),
1108            &GraphIdentityText::new("src/lib.rs")?,
1109        );
1110        let entity_export =
1111            EntityResolutionKey::new(file_entity.key().clone(), resolution_key.clone())?;
1112        let relation_dependency =
1113            RelationDependencyKey::new(relation.key().clone(), resolution_key)?;
1114        let mut publication = store.begin_index_publication("identity-transition")?;
1115        publication.replace_repository_graph_with_resolution_keys(
1116            project,
1117            &[project_entity, file_entity],
1118            &[relation],
1119            &[occurrence],
1120            &[coverage],
1121            &[entity_export],
1122            &[relation_dependency],
1123        )?;
1124        publication.complete()?;
1125        Ok(())
1126    }
1127
1128    fn assert_authored_state(store: &AtlasStore) -> Result<(), Box<dyn Error>> {
1129        let mut nodes = store
1130            .connection
1131            .prepare("SELECT path, kind, parent_path FROM nodes ORDER BY path")?;
1132        let nodes = nodes
1133            .query_map([], |row| {
1134                Ok((
1135                    row.get::<_, String>(0)?,
1136                    row.get::<_, String>(1)?,
1137                    row.get::<_, Option<String>>(2)?,
1138                ))
1139            })?
1140            .collect::<Result<Vec<_>, _>>()?;
1141        require_eq(
1142            &nodes,
1143            &vec![
1144                (".".to_string(), "folder".to_string(), None),
1145                (
1146                    "src/lib.rs".to_string(),
1147                    "file".to_string(),
1148                    Some(".".to_string()),
1149                ),
1150            ],
1151            "node anchors",
1152        )?;
1153
1154        let purpose = store.connection.query_row(
1155            "SELECT n.path, p.purpose, p.source, p.status, p.updated_by
1156               FROM purposes p JOIN nodes n ON n.id = p.node_id",
1157            [],
1158            |row| {
1159                Ok((
1160                    row.get::<_, String>(0)?,
1161                    row.get::<_, Option<String>>(1)?,
1162                    row.get::<_, String>(2)?,
1163                    row.get::<_, String>(3)?,
1164                    row.get::<_, Option<String>>(4)?,
1165                ))
1166            },
1167        )?;
1168        require_eq(
1169            &purpose,
1170            &(
1171                "src/lib.rs".to_string(),
1172                Some("Own the local source entry point.".to_string()),
1173                "agent".to_string(),
1174                "approved".to_string(),
1175                Some("agent".to_string()),
1176            ),
1177            "purpose content and review ownership",
1178        )?;
1179
1180        let custom_setting = store.connection.query_row(
1181            "SELECT value FROM metadata WHERE key = 'custom.identity-test-setting'",
1182            [],
1183            |row| row.get::<_, String>(0),
1184        )?;
1185        require_eq(&custom_setting, &"retain-me".to_string(), "custom metadata")?;
1186
1187        let health_resolution = store.connection.query_row(
1188            "SELECT finding_id, category, path, related_path, rationale, resolved_by
1189               FROM health_resolutions",
1190            [],
1191            |row| {
1192                Ok((
1193                    row.get::<_, String>(0)?,
1194                    row.get::<_, String>(1)?,
1195                    row.get::<_, String>(2)?,
1196                    row.get::<_, Option<String>>(3)?,
1197                    row.get::<_, String>(4)?,
1198                    row.get::<_, String>(5)?,
1199                ))
1200            },
1201        )?;
1202        require_eq(
1203            &health_resolution,
1204            &(
1205                "resolved-purpose".to_string(),
1206                "duplicate-purpose".to_string(),
1207                "src/lib.rs".to_string(),
1208                Some("src/main.rs".to_string()),
1209                "The responsibilities are intentionally distinct.".to_string(),
1210                "agent".to_string(),
1211            ),
1212            "health resolution content",
1213        )?;
1214
1215        Ok(())
1216    }
1217
1218    /// Build the modeled event used to prove project-scoped runtime lifecycle.
1219    fn identity_transition_usage_event() -> projectatlas_core::telemetry::UsageEvent {
1220        usage_from_estimates(
1221            "identity-test",
1222            "summary",
1223            Some("src/lib.rs".to_string()),
1224            Some("identity transition".to_string()),
1225            120,
1226            20,
1227        )
1228    }
1229
1230    /// Assert the current project report without reading schema-private raw columns.
1231    fn assert_usage_report(store: &AtlasStore, retained: bool) -> Result<(), Box<dyn Error>> {
1232        let events = store.usage_events(Some("identity-test"))?;
1233        let overview = store.token_overview(Some("identity-test"))?;
1234        if retained {
1235            require_eq(
1236                &events,
1237                &vec![identity_transition_usage_event()],
1238                "project-scoped usage event",
1239            )?;
1240            require_eq(&overview.calls, &1, "project-scoped usage calls")?;
1241        } else {
1242            require(
1243                events.is_empty(),
1244                "detached project exposed prior raw usage",
1245            )?;
1246            require_eq(&overview.calls, &0, "detached project usage calls")?;
1247        }
1248        Ok(())
1249    }
1250
1251    /// Assert active/sealed instance and baseline ownership for one project identity.
1252    fn assert_runtime_scope(
1253        store: &AtlasStore,
1254        project: ProjectInstanceId,
1255        expected_active: i64,
1256        expected_sealed: i64,
1257        expected_baselines: i64,
1258    ) -> Result<(), Box<dyn Error>> {
1259        const ACTIVE: &str = "active";
1260        const SEALED: &str = "sealed";
1261        let project_bytes = project.as_bytes();
1262        let (active, sealed) = store.connection.query_row(
1263            "SELECT
1264                 COALESCE(SUM(CASE WHEN state = ?2 THEN 1 ELSE 0 END), 0),
1265                 COALESCE(SUM(CASE WHEN state = ?3 THEN 1 ELSE 0 END), 0)
1266             FROM usage_instances
1267             WHERE project_instance_id = ?1",
1268            rusqlite::params![project_bytes.as_slice(), ACTIVE, SEALED],
1269            |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
1270        )?;
1271        let baselines = store.connection.query_row(
1272            "SELECT COUNT(*)
1273             FROM usage_instance_baselines AS b
1274             JOIN usage_instances AS i USING(instance_row_id)
1275             WHERE i.project_instance_id = ?1",
1276            [project_bytes.as_slice()],
1277            |row| row.get::<_, i64>(0),
1278        )?;
1279        require_eq(&active, &expected_active, "active runtime instances")?;
1280        require_eq(&sealed, &expected_sealed, "sealed runtime instances")?;
1281        require_eq(&baselines, &expected_baselines, "active baseline witnesses")
1282    }
1283
1284    /// Require that a rejected transition left the main database bytes unchanged.
1285    fn assert_database_unchanged(
1286        database_path: &Path,
1287        expected: &[u8],
1288        label: &str,
1289    ) -> Result<(), Box<dyn Error>> {
1290        require_eq(&fs::read(database_path)?, &expected.to_vec(), label)
1291    }
1292
1293    fn assert_graph_counts(store: &AtlasStore, expected: [i64; 7]) -> Result<(), Box<dyn Error>> {
1294        let mut counts = Vec::new();
1295        for table in [
1296            "graph_entities",
1297            "graph_relations",
1298            "graph_relation_occurrences",
1299            "graph_coverage",
1300            "graph_resolution_keys",
1301            "graph_entity_exports",
1302            "graph_relation_dependencies",
1303        ] {
1304            counts.push(store.connection.query_row(
1305                &format!("SELECT COUNT(*) FROM {table}"),
1306                [],
1307                |row| row.get::<_, i64>(0),
1308            )?);
1309        }
1310        require_eq(&counts, &expected.to_vec(), "graph row counts")?;
1311        let quick_check = store
1312            .connection
1313            .query_row("PRAGMA quick_check(1)", [], |row| row.get::<_, String>(0))?;
1314        require_eq(&quick_check, &"ok".to_string(), "database integrity")?;
1315        let foreign_key_failures = store.connection.query_row(
1316            "SELECT COUNT(*) FROM pragma_foreign_key_check",
1317            [],
1318            |row| row.get::<_, i64>(0),
1319        )?;
1320        require_eq(&foreign_key_failures, &0, "foreign key integrity")?;
1321        Ok(())
1322    }
1323}