Skip to main content

lean_ctx/core/
vault_map.rs

1// SPDX-License-Identifier: Apache-2.0
2//! `<data_dir>/hive/projects.json`: the vaults Hive resolved for this machine's repositories
3//! and the local checkouts seen for each. Written when a checkout is resolved; read wherever a
4//! checkout's vault matters (bus, sync, and the per-vault policy floor).
5
6use std::collections::BTreeMap;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11use lean_ctx_protocol::hive::vault::VaultV1;
12
13/// The file keeps its pre-VA name, like the field names in it: an older
14/// engine (downgrade, or a teammate's machine) still reads it.
15pub const VAULTS_FILE: &str = "projects.json";
16
17/// A vault as Hive resolved it, plus the local checkouts seen for it.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct VaultRecord {
20    /// On disk as `project_id` (pre-VA name) for downgrades and mixed-version teams.
21    #[serde(rename = "project_id", alias = "vault_id")]
22    pub vault_id: String,
23    pub ns: String,
24    pub name: String,
25    /// Always `false` (sealed vaults are not supported); `projects.json`
26    /// carries the field.
27    pub sealed: bool,
28    #[serde(default)]
29    pub roots: Vec<String>,
30}
31
32/// `<data_dir>/hive/projects.json`: stable identity → resolved vault.
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct VaultMap {
35    /// On disk as `projects` (pre-VA name) for downgrades and mixed-version teams.
36    #[serde(default, rename = "projects", alias = "vaults")]
37    pub vaults: BTreeMap<String, VaultRecord>,
38}
39
40impl VaultMap {
41    pub fn load(data_dir: &Path) -> Self {
42        std::fs::read(data_dir.join(VAULTS_FILE))
43            .ok()
44            .and_then(|bytes| serde_json::from_slice(&bytes).ok())
45            .unwrap_or_default()
46    }
47
48    /// The vault of the local checkout `root`: the one recorded for it, else
49    /// (a linked git worktree never synced itself) the one of its main
50    /// checkout, which is the same repository.
51    pub fn for_root(&self, root: &str) -> Option<&VaultRecord> {
52        let find = |root: &str| {
53            self.vaults.values().find(|record| {
54                record
55                    .roots
56                    .iter()
57                    .any(|known| crate::core::pathutil::same_project(known, root))
58            })
59        };
60        find(root).or_else(|| {
61            let main = crate::core::git_worktree::main_worktree_root(Path::new(root))?;
62            find(&main.to_string_lossy())
63        })
64    }
65
66    pub fn save(&self, data_dir: &Path) -> std::io::Result<()> {
67        let bytes = serde_json::to_vec_pretty(self).map_err(std::io::Error::other)?;
68        crate::core::secure_fs::write_private(data_dir, VAULTS_FILE, &bytes)
69    }
70
71    /// Record a resolve answer for `stable`, remembering `root`.
72    pub fn record(&mut self, stable: &str, vault: &VaultV1, root: &Path) {
73        let root = root.to_string_lossy().into_owned();
74        let entry = self
75            .vaults
76            .entry(stable.to_string())
77            .or_insert_with(|| VaultRecord {
78                vault_id: vault.vault_id.clone(),
79                ns: vault.vault.ns.clone(),
80                name: vault.vault.name.clone(),
81                sealed: false,
82                roots: Vec::new(),
83            });
84        entry.vault_id.clone_from(&vault.vault_id);
85        entry.ns.clone_from(&vault.vault.ns);
86        entry.name.clone_from(&vault.vault.name);
87        entry.sealed = false;
88        if !entry.roots.contains(&root) {
89            entry.roots.push(root);
90        }
91    }
92}