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