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>;