Skip to main content

greplm_core/
meta.rs

1//! Index manifest persisted at `.greplm/meta.json`.
2
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::error::{Error, Result};
8
9/// On-disk format version. Bump when the segment layout changes.
10///
11/// - v2 added the per-segment `seg-N.refs` reference/call-edge table.
12/// - v3 switched the `docs`/`syms`/`refs` side tables from JSON to postcard
13///   (compact binary) to cut on-disk size and cold-start parse time.
14pub const SCHEMA_VERSION: u32 = 3;
15
16/// Index-wide manifest describing the set of live segments.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Meta {
19    pub schema_version: u32,
20    /// IDs of segments that make up the current index.
21    pub segments: Vec<u64>,
22    /// Monotonic counter for allocating new segment IDs.
23    pub next_segment_id: u64,
24    /// Unix timestamp (seconds) of the last successful index operation.
25    pub last_indexed: u64,
26    /// Total live documents across all segments.
27    pub doc_count: u64,
28    /// Total symbols across all segments.
29    pub symbol_count: u64,
30    /// Git commit sha at the time of indexing (empty if not a repo). Lets
31    /// callers detect that the working tree moved (e.g. a branch switch).
32    #[serde(default)]
33    pub indexed_git_head: String,
34    /// Git branch name at the time of indexing (empty if not a repo).
35    #[serde(default)]
36    pub indexed_branch: String,
37}
38
39impl Default for Meta {
40    fn default() -> Self {
41        Self {
42            schema_version: SCHEMA_VERSION,
43            segments: Vec::new(),
44            next_segment_id: 0,
45            last_indexed: 0,
46            doc_count: 0,
47            symbol_count: 0,
48            indexed_git_head: String::new(),
49            indexed_branch: String::new(),
50        }
51    }
52}
53
54impl Meta {
55    pub fn load(path: &Path) -> Result<Meta> {
56        match std::fs::read(path) {
57            Ok(bytes) => {
58                let meta: Meta = serde_json::from_slice(&bytes)?;
59                if meta.schema_version != SCHEMA_VERSION {
60                    return Err(Error::Corrupt(format!(
61                        "index schema version {} != supported {}; run `greplm index` to rebuild",
62                        meta.schema_version, SCHEMA_VERSION
63                    )));
64                }
65                Ok(meta)
66            }
67            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Meta::default()),
68            Err(e) => Err(Error::io(path, e)),
69        }
70    }
71
72    pub fn save(&self, path: &Path) -> Result<()> {
73        let bytes = serde_json::to_vec_pretty(self)?;
74        crate::fsutil::write_atomic(path, &bytes)
75    }
76
77    /// Record the current git HEAD/branch for the indexed tree (best-effort).
78    pub fn record_git_head(&mut self, root: &Path) {
79        if let Some((sha, branch)) = crate::git::head(root) {
80            self.indexed_git_head = sha;
81            self.indexed_branch = branch;
82        }
83    }
84
85    pub fn touch_now(&mut self) {
86        self.last_indexed = SystemTime::now()
87            .duration_since(UNIX_EPOCH)
88            .map(|d| d.as_secs())
89            .unwrap_or(0);
90    }
91
92    pub fn alloc_segment(&mut self) -> u64 {
93        let id = self.next_segment_id;
94        self.next_segment_id += 1;
95        id
96    }
97}