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