Skip to main content

scope_engine/
api.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4// ── Domain types ────────────────────────────────────────────
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct OpenProjectOutput {
8    pub status: String,
9    pub project_root: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub detected_lsp_language: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub lsp: Option<String>,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ReadCodeInput {
19    pub path: String,
20    pub anchor: String,
21    pub mode: ReadCodeMode,
22}
23
24#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
25#[serde(rename_all = "lowercase")]
26pub enum ReadCodeMode {
27    Around,
28    Full,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct ReadCodeOutput {
33    /// File content with per-line hash prefix: `line#hash|original_text\n`
34    pub content: String,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct SearchCodeOutput {
39    pub matches: Vec<SearchHit>,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
43pub struct SearchHit {
44    pub path: String,
45    pub hit: String,
46}
47
48#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
49#[serde(rename_all = "lowercase")]
50pub enum SearchMode {
51    #[default]
52    Literal,
53    Regex,
54}
55
56#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
57#[serde(rename_all = "lowercase")]
58pub enum SearchCase {
59    Sensitive,
60    Insensitive,
61    #[default]
62    Smart,
63}
64
65#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
66#[serde(transparent)]
67#[schemars(transparent)]
68pub struct SearchFlag(bool);
69
70impl SearchFlag {
71    #[must_use]
72    pub const fn is_enabled(self) -> bool {
73        self.0
74    }
75}
76
77impl From<bool> for SearchFlag {
78    fn from(value: bool) -> Self {
79        Self(value)
80    }
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
84pub struct SearchCodeInput {
85    pub query: String,
86    #[serde(default)]
87    pub mode: SearchMode,
88    pub path: Option<String>,
89    #[serde(default, deserialize_with = "deserialize_string_list")]
90    pub include: Vec<String>,
91    #[serde(default, deserialize_with = "deserialize_string_list")]
92    pub exclude: Vec<String>,
93    #[serde(default, deserialize_with = "deserialize_string_list")]
94    pub types: Vec<String>,
95    #[serde(default, deserialize_with = "deserialize_string_list")]
96    pub type_not: Vec<String>,
97    #[serde(default, rename = "case")]
98    pub case_mode: SearchCase,
99    #[serde(default)]
100    pub word: bool,
101    #[serde(default)]
102    pub whole_line: bool,
103    #[serde(default)]
104    pub hidden: SearchFlag,
105    #[serde(default = "default_respect_ignore")]
106    pub respect_ignore: bool,
107    #[serde(default)]
108    pub follow: SearchFlag,
109    pub limit: Option<usize>,
110}
111
112impl Default for SearchCodeInput {
113    fn default() -> Self {
114        Self {
115            query: String::new(),
116            mode: SearchMode::default(),
117            path: None,
118            include: Vec::new(),
119            exclude: Vec::new(),
120            types: Vec::new(),
121            type_not: Vec::new(),
122            case_mode: SearchCase::default(),
123            word: false,
124            whole_line: false,
125            hidden: false.into(),
126            respect_ignore: true,
127            follow: false.into(),
128            limit: None,
129        }
130    }
131}
132
133const fn default_respect_ignore() -> bool {
134    true
135}
136
137fn deserialize_string_list<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
138where
139    D: Deserializer<'de>,
140{
141    #[derive(Deserialize)]
142    #[serde(untagged)]
143    enum StringList {
144        One(String),
145        Many(Vec<String>),
146    }
147
148    let Some(value) = Option::<StringList>::deserialize(deserializer)? else {
149        return Ok(Vec::new());
150    };
151    Ok(match value {
152        StringList::One(value) => vec![value],
153        StringList::Many(values) => values,
154    })
155}
156
157#[derive(Debug, Clone, Deserialize)]
158pub struct EditCodeInput {
159    pub edits: Vec<StructuredEdit>,
160}
161
162/// Operation kind for a single structured edit.
163#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
164#[serde(rename_all = "lowercase")]
165pub enum EditOp {
166    Replace,
167    Append,
168    Prepend,
169}
170
171/// Content value: string, array of strings, or null.
172#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
173#[serde(untagged)]
174pub enum EditContent {
175    Lines(Vec<String>),
176    Text(String),
177}
178
179impl EditContent {
180    pub fn into_lines(self) -> Vec<String> {
181        match self {
182            Self::Lines(lines) => lines,
183            Self::Text(text) => text.lines().map(str::to_string).collect(),
184        }
185    }
186}
187
188/// One structured edit: op + path + line-hash anchors + optional content.
189#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
190pub struct StructuredEdit {
191    /// Relative file path from project root.
192    pub path: String,
193    /// Operation kind (auto-defaults to `append` for new files when omitted).
194    #[serde(default)]
195    pub op: Option<EditOp>,
196    /// `line#hash` anchor from `read_code` output.
197    /// For new files this can be omitted (defaults to `"1#"`).
198    #[serde(default)]
199    pub start: Option<String>,
200    /// `line#hash` end anchor (required for replace, ignored otherwise).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub end: Option<String>,
203    /// Replacement/insertion content as string, array, or null.
204    /// `null` with `replace` means delete.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub content: Option<EditContent>,
207}
208
209#[derive(Debug, Clone, Deserialize)]
210pub struct SourceResponsibilityInput {
211    pub path: String,
212}
213
214#[derive(Debug, Clone, Deserialize, Serialize)]
215pub struct SourceResponsibility {
216    pub is_responsible: bool,
217    pub path: String,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub extension: Option<String>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub language: Option<String>,
222    pub reason: String,
223}
224
225// ── Propagation types ────────────────────────────────────────
226
227#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
228#[serde(rename_all = "snake_case")]
229pub enum PropagationSource {
230    Lsp,
231    OpenEnded,
232}
233
234#[derive(Debug, Clone, Serialize)]
235pub struct EditCodeOutput {
236    pub propagation_results: Vec<PropagationResult>,
237    pub applied_summary: AppliedStructuredEditSummary,
238}
239
240#[derive(Debug, Clone, Serialize)]
241pub struct PropagationResult {
242    pub selector: String,
243    pub reason: String,
244    pub source: PropagationSource,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub lsp_references: Option<Vec<(String, usize, String)>>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub diff_summary: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub file_snippet: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub project_files: Option<Vec<String>>,
253}
254
255#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
256#[serde(rename_all = "snake_case")]
257pub enum AppliedStructuredEditOperation {
258    Add,
259    Update,
260}
261
262#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
263pub struct AppliedStructuredEditFile {
264    pub path: String,
265    pub operation: AppliedStructuredEditOperation,
266    pub added_lines: usize,
267    pub removed_lines: usize,
268    pub original_content: String,
269    pub new_content: String,
270}
271
272#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
273pub struct AppliedStructuredEditSummary {
274    pub files: Vec<AppliedStructuredEditFile>,
275}
276
277#[derive(Debug, Clone, Serialize)]
278pub struct ReviewBatch {
279    pub review: Option<ReviewEvent>,
280    pub reviews: Vec<ReviewEvent>,
281    pub returned: usize,
282    pub remaining: usize,
283}
284
285#[derive(Debug, Clone, Serialize)]
286pub struct Reference {
287    pub selector: String,
288    pub line: usize,
289    pub context: String,
290}
291
292#[derive(Debug, Clone, Serialize)]
293#[serde(tag = "type")]
294#[serde(rename_all = "snake_case")]
295pub enum ReviewEvent {
296    KnownReferences {
297        modified_symbol: String,
298        change_summary: String,
299        references: Vec<Reference>,
300        file_snippet: String,
301    },
302    InvestigateImpact {
303        modified_symbol: String,
304        change_summary: String,
305        diff_summary: String,
306        file_snippet: String,
307        project_files: Vec<String>,
308    },
309}