Skip to main content

grex_core/lockfile/
entry.rs

1//! Lockfile entry + error types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// One resolved pack entry. Serialized as a single JSON line.
8///
9/// Marked `#[non_exhaustive]` so future audit fields (timestamps,
10/// resolved-ref metadata, plugin signatures) can land without breaking
11/// out-of-crate consumers that struct-literal-construct entries.
12/// Within `grex-core` the existing struct-literal sites continue to
13/// work unchanged; external callers should use [`LockEntry::new`] (and
14/// the field-level `pub` mutators) instead of struct literals.
15#[non_exhaustive]
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
17pub struct LockEntry {
18    /// Pack identifier — matches the manifest id.
19    pub id: String,
20    /// Resolved commit SHA at the time of install.
21    pub sha: String,
22    /// Branch or ref used to resolve `sha`.
23    pub branch: String,
24    /// Timestamp of the last successful install/sync.
25    pub installed_at: DateTime<Utc>,
26    /// Content hash of the declarative actions that ran. Empty for
27    /// imperative packs.
28    pub actions_hash: String,
29    /// Schema version of this entry.
30    pub schema_version: String,
31    /// `true` when the walker synthesised this pack's manifest in-memory
32    /// because the child had no `.grex/pack.yaml` but did carry a
33    /// `.git/` (v1.1.1 plain-git children). `#[serde(default)]` keeps
34    /// pre-v1.1.1 lockfiles forward-compatible — a missing field
35    /// deserialises to `false`. See
36    /// `openspec/changes/feat-v1.1.1-plain-git-children/design.md`.
37    #[serde(default)]
38    pub synthetic: bool,
39}
40
41impl LockEntry {
42    /// Construct a new entry with every required field. The `synthetic`
43    /// flag defaults to `false`; callers that need a synthetic entry
44    /// should set the field directly after construction (the field is
45    /// `pub`).
46    ///
47    /// Because [`LockEntry`] is `#[non_exhaustive]`, this is the
48    /// canonical constructor for out-of-crate consumers — struct
49    /// literals will not compile from outside the crate.
50    #[must_use]
51    pub fn new(
52        id: impl Into<String>,
53        sha: impl Into<String>,
54        branch: impl Into<String>,
55        installed_at: DateTime<Utc>,
56        actions_hash: impl Into<String>,
57        schema_version: impl Into<String>,
58    ) -> Self {
59        Self {
60            id: id.into(),
61            sha: sha.into(),
62            branch: branch.into(),
63            installed_at,
64            actions_hash: actions_hash.into(),
65            schema_version: schema_version.into(),
66            synthetic: false,
67        }
68    }
69}
70
71/// Errors surfaced by lockfile read/write.
72#[derive(Debug, Error)]
73pub enum LockfileError {
74    /// I/O failure while reading or writing.
75    #[error("lockfile i/o error: {0}")]
76    Io(#[from] std::io::Error),
77
78    /// A line failed to parse. Lockfile corruption is always fatal — there
79    /// is no torn-line recovery rule since writes are atomic.
80    #[error("lockfile corrupted at line {line}: {source}")]
81    Corruption {
82        /// 1-based line number.
83        line: usize,
84        /// Underlying JSON parse error.
85        #[source]
86        source: serde_json::Error,
87    },
88
89    /// Serialization failure when writing.
90    #[error("lockfile serialize error: {0}")]
91    Serialize(serde_json::Error),
92}