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}