Skip to main content

mlua_swarm/store/issue/
mod.rs

1//! `IssueStore` — the persistence abstraction for enhance requests (Issues).
2//!
3//! Same layer as `BPStore`. Dedicated to CRUD and status lookup on
4//! Issues. The old `enhance::issue::IssueSource`'s acquire/release
5//! queue semantics are gone — the shape now has `EnhancePP` fetch
6//! directly.
7//!
8//! Current scope:
9//!
10//! - `InMemoryIssueStore` — process-volatile default.
11//! - `SqliteIssueStore` — file-backed persistence via `rusqlite-isle`
12//!   (thread-isolated `Connection`, single-writer FIFO discipline).
13//! - Other persistent backends (Git / mini-app / …) are future carries.
14
15use crate::blueprint::store::BlueprintId;
16use async_trait::async_trait;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::sync::Mutex;
20use thiserror::Error;
21
22pub mod inmemory;
23pub mod sqlite;
24pub use inmemory::InMemoryIssueStore;
25pub use sqlite::SqliteIssueStore;
26
27// ──────────────────────────────────────────────────────────────────────────
28// IssueId / IssuePayload / IssueStatus
29// ──────────────────────────────────────────────────────────────────────────
30
31/// Issue identifier — the human-facing id for an enhance request.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct IssueId(pub String);
34
35impl IssueId {
36    /// Wrap an arbitrary string as an id.
37    pub fn new<S: Into<String>>(s: S) -> Self {
38        Self(s.into())
39    }
40
41    /// Borrow the inner string.
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47impl std::fmt::Display for IssueId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", self.0)
50    }
51}
52
53/// The unit of work the Enhance loop processes — a request that
54/// says "please modify Blueprint X".
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct IssuePayload {
57    /// Issue identifier.
58    pub issue_id: IssueId,
59    /// The Blueprint to be modified.
60    pub blueprint_id: BlueprintId,
61    /// Modification intent / context — the natural-language prompt
62    /// passed on to the `PatchSpawner`.
63    pub intent: String,
64}
65
66/// Lifecycle state of an Issue.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub enum IssueStatus {
69    /// Submitted, not yet processed.
70    Pending,
71    /// In progress.
72    InFlight,
73    /// Complete — patch applied. Carries the new BP commit id.
74    Applied {
75        /// The new Blueprint commit id after the patch was applied.
76        new_version: String,
77    },
78    /// Rejected. Carries a reason.
79    Rejected {
80        /// Why the Issue was rejected.
81        reason: String,
82    },
83}
84
85/// Errors surfaced by an [`IssueStore`] implementation.
86#[derive(Debug, Error)]
87pub enum IssueStoreError {
88    /// No Issue exists for the given id.
89    #[error("issue not found: {0}")]
90    NotFound(IssueId),
91
92    /// `create` was called with an id that is already stored.
93    #[error("issue already exists: {0}")]
94    Duplicate(IssueId),
95
96    /// Backend-specific failure not covered by the other variants.
97    #[error("other: {0}")]
98    Other(String),
99}
100
101// ──────────────────────────────────────────────────────────────────────────
102// IssueStore trait
103// ──────────────────────────────────────────────────────────────────────────
104
105/// Persistence interface for Issues — same layer as `BPStore`.
106#[async_trait]
107pub trait IssueStore: Send + Sync {
108    /// Backend name — for diagnostics/logging.
109    fn name(&self) -> &str;
110
111    /// Submit a new Issue with `status = Pending`.
112    async fn create(&self, payload: IssuePayload) -> Result<(), IssueStoreError>;
113
114    /// Fetch the Issue body.
115    async fn get(&self, id: &IssueId) -> Result<IssuePayload, IssueStoreError>;
116
117    /// Fetch the Issue's status; returns `NotFound` when absent.
118    async fn status(&self, id: &IssueId) -> Result<IssueStatus, IssueStoreError>;
119
120    /// List every Issue in insertion order — for audit and debug.
121    async fn list(&self) -> Result<Vec<(IssueId, IssueStatus)>, IssueStoreError>;
122
123    /// Pop one pending Issue (FIFO) — used by `EnhancePP` for
124    /// dispatch. Transitions the status to `InFlight` on pop. Returns
125    /// `Ok(None)` when there is no work.
126    async fn pop_pending(&self) -> Result<Option<IssuePayload>, IssueStoreError>;
127
128    /// Update an Issue's status — the terminal transitions to
129    /// `Applied` / `Rejected` and so on.
130    async fn update_status(&self, id: &IssueId, status: IssueStatus)
131        -> Result<(), IssueStoreError>;
132}
133
134// ──────────────────────────────────────────────────────────────────────────
135// Shared inner state used by the InMemory backend.
136// ──────────────────────────────────────────────────────────────────────────
137
138#[derive(Default)]
139pub(crate) struct Inner {
140    /// Insertion order — audit / list use.
141    pub(crate) order: Vec<IssueId>,
142    pub(crate) payloads: HashMap<IssueId, IssuePayload>,
143    pub(crate) statuses: HashMap<IssueId, IssueStatus>,
144    /// Pending FIFO queue.
145    pub(crate) pending: std::collections::VecDeque<IssueId>,
146}
147
148pub(crate) type SharedInner = Mutex<Inner>;