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}