Skip to main content

mlua_swarm/blueprint/
store.rs

1//! `BlueprintStore` — the Blueprint VCS abstraction.
2//!
3//! The interface (the `BlueprintStore` trait) does not leak Git concepts
4//! (`commit` / `tree` / `refs`). `BlueprintVersion` is abstracted as a
5//! `ContentHash` (blake3), leaving room to pick between Git2, InMemory,
6//! and future File / Remote / Lua backends.
7//!
8//! Current scope:
9//! - `Git2BlueprintStore` (`Layout::Single`).
10//! - `InMemoryBlueprintStore` (for tests).
11//! - Canonical form = `serde_yaml::to_string` — the definitive form.
12//! - `ContentHash` = `blake3(canonical bytes)`.
13
14use crate::blueprint::Blueprint;
15use async_trait::async_trait;
16
17pub mod types;
18
19pub use types::{
20    BlueprintEpoch, BlueprintId, BlueprintStoreError, BlueprintVersion, CommitMetadata,
21    ContentHash, Trace, TraceOrigin, TraceRef, Traced,
22};
23
24pub mod git2_store;
25pub mod inmemory;
26
27pub use git2_store::Git2BlueprintStore;
28pub use inmemory::InMemoryBlueprintStore;
29pub(crate) mod git2_blob_store;
30
31// ──────────────────────────────────────────────────────────────────────────
32// BlueprintStore trait
33// ──────────────────────────────────────────────────────────────────────────
34
35/// The Blueprint-VCS abstract interface. Backed by Git2, InMemory,
36/// Remote, and so on.
37///
38/// # Design principles
39///
40/// - Do not leak Git concepts (`commit` / `tree` / `refs`) into the
41///   interface.
42/// - Abstract versions as `BlueprintVersion` = `ContentHash` (blake3).
43/// - Keep the surface async, matching the engine's existing traits.
44#[async_trait]
45pub trait BlueprintStore: Send + Sync {
46    /// Backend identifier, for logging / diagnostics (e.g. `"git2"`,
47    /// `"in-memory"`).
48    fn name(&self) -> &str;
49
50    /// Read the current head — the latest commit for this `BlueprintId`.
51    async fn read_head(&self, id: &BlueprintId) -> Result<Traced<Blueprint>, BlueprintStoreError>;
52
53    /// Append a new Blueprint. Computes the `ContentHash` and returns the
54    /// resulting `BlueprintVersion`.
55    async fn write_new(
56        &self,
57        id: &BlueprintId,
58        new_bp: &Blueprint,
59        parents: &[BlueprintVersion],
60        metadata: CommitMetadata,
61    ) -> Result<BlueprintVersion, BlueprintStoreError>;
62
63    /// Look up a past version — used for audit or debug.
64    async fn read_version(
65        &self,
66        id: &BlueprintId,
67        version: BlueprintVersion,
68    ) -> Result<Traced<Blueprint>, BlueprintStoreError>;
69
70    /// List history newest-to-oldest, up to `limit`; head included.
71    async fn history(
72        &self,
73        id: &BlueprintId,
74        limit: usize,
75    ) -> Result<Vec<BlueprintVersion>, BlueprintStoreError>;
76
77    /// Return the rationale attached to a commit (its
78    /// `CommitMetadata.rationale`, a one-line changelog). Backends that
79    /// cannot recover it return `Ok(None)`; the trait's default
80    /// implementation returns `None`.
81    async fn read_commit_rationale(
82        &self,
83        _id: &BlueprintId,
84        _version: BlueprintVersion,
85    ) -> Result<Option<String>, BlueprintStoreError> {
86        Ok(None)
87    }
88
89    /// List every `BlueprintId` (relevant when the `Layout::Multi` axis
90    /// is in use).
91    async fn list_ids(&self) -> Result<Vec<BlueprintId>, BlueprintStoreError>;
92
93    /// Archive a `BlueprintId` — logical soft-delete via an archive
94    /// marker commit. After archive, [`read_head`] returns
95    /// [`BlueprintStoreError::Archived`], [`list_ids`] filters the id
96    /// out by default, and downstream resolvers (e.g. `swarm_run(id)`)
97    /// hard-reject with the same error.
98    ///
99    /// Restoring is symmetric — [`unarchive_id`] appends an unarchive
100    /// marker commit and re-exposes the id. History is preserved end-to-
101    /// end; nothing is physically removed.
102    ///
103    /// Default implementation returns
104    /// [`BlueprintStoreError::Unsupported`]; only backends that support
105    /// archive semantics (Git2) override.
106    ///
107    /// [`read_head`]: BlueprintStore::read_head
108    /// [`list_ids`]: BlueprintStore::list_ids
109    /// [`unarchive_id`]: BlueprintStore::unarchive_id
110    async fn archive_id(&self, _id: &BlueprintId) -> Result<(), BlueprintStoreError> {
111        Err(BlueprintStoreError::Unsupported(
112            "archive_id is not supported by this backend".into(),
113        ))
114    }
115
116    /// Reverse of [`archive_id`] — append an unarchive marker commit to
117    /// the main head and re-expose the id.
118    ///
119    /// [`archive_id`]: BlueprintStore::archive_id
120    async fn unarchive_id(&self, _id: &BlueprintId) -> Result<(), BlueprintStoreError> {
121        Err(BlueprintStoreError::Unsupported(
122            "unarchive_id is not supported by this backend".into(),
123        ))
124    }
125
126    /// Return `true` if the id is currently archived. Default returns
127    /// `Ok(false)` for backends that never archive.
128    async fn is_archived(&self, _id: &BlueprintId) -> Result<bool, BlueprintStoreError> {
129        Ok(false)
130    }
131}
132
133// ──────────────────────────────────────────────────────────────────────────
134// canonical helpers (serde_yaml::to_string Definitive form)
135// ──────────────────────────────────────────────────────────────────────────
136
137/// Serialise a Blueprint to canonical YAML bytes — the definitive form.
138/// The same output feeds both the `ContentHash` computation and the Git
139/// commit blob.
140pub fn canonical_yaml(bp: &Blueprint) -> Result<String, BlueprintStoreError> {
141    Ok(serde_yaml::to_string(bp)?)
142}
143
144/// Compute the Blueprint's `ContentHash` — `blake3` of the canonical
145/// YAML bytes.
146pub fn blueprint_content_hash(bp: &Blueprint) -> Result<ContentHash, BlueprintStoreError> {
147    let yaml = canonical_yaml(bp)?;
148    Ok(ContentHash::from_bytes(yaml.as_bytes()))
149}
150
151/// Compute the Blueprint's `BlueprintVersion` — a newtype wrapper around
152/// `ContentHash`.
153pub fn blueprint_version(bp: &Blueprint) -> Result<BlueprintVersion, BlueprintStoreError> {
154    Ok(BlueprintVersion(blueprint_content_hash(bp)?))
155}