1#![allow(
4 clippy::missing_errors_doc,
5 clippy::missing_panics_doc,
6 clippy::too_many_lines
7)]
8
9use std::collections::BTreeMap;
10use std::fmt;
11use std::path::Path;
12
13use presolve_compiler::persistent_cache::CacheReportSelector;
14use presolve_compiler::platform::{
15 derive_workspace_id_v1, ContractVersion, RequestedCompilationMode, WorkspaceInput,
16 WorkspaceSnapshot, WorkspaceSource,
17};
18use presolve_compiler::service::{
19 CompileRequest, CompilerServiceHost, CompleteSource, IncrementalReportSelector,
20 WatchCandidateV1, WatchChangeBatchV1, WorkspaceCompileRequestV1,
21 WorkspacePackageCompileRequestV1,
22};
23use presolve_compiler::watch::{
24 ObservedChangeV1, WatchDebounceV1, WatchSessionConfigurationV1, WATCH_CHANGE_BATCH_V1_SCHEMA,
25 WATCH_SESSION_CONFIGURATION_V1_SCHEMA,
26};
27use presolve_compiler::workspace::{
28 WorkspaceManifestV1, WorkspacePackageDescriptorV1, WorkspacePolicyV1,
29 WORKSPACE_MANIFEST_V1_SCHEMA,
30};
31
32use crate::{
33 load_explicit_project_envelope_v1, load_explicit_source_inputs_v1, CliExplicitSourceSpecV1,
34};
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CliWorkspaceResultV1 {
38 pub workspace_id: String,
39 pub status: String,
40 pub manifest_identity: String,
41 pub graph_identity: String,
42 pub plan_identity: String,
43 pub package_snapshot_id: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct CliWorkspaceErrorV1 {
48 pub code: &'static str,
49 pub message: String,
50}
51
52impl fmt::Display for CliWorkspaceErrorV1 {
53 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(output, "{}: {}", self.code, self.message)
55 }
56}
57
58impl std::error::Error for CliWorkspaceErrorV1 {}
59
60fn compiler_contract() -> ContractVersion {
61 ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION")))
62}
63
64pub fn run_explicit_workspace_v1(
67 configuration_path: &Path,
68 source_specs: &[CliExplicitSourceSpecV1],
69 verify_clean_equivalence: bool,
70) -> Result<CliWorkspaceResultV1, CliWorkspaceErrorV1> {
71 let project_root = configuration_path.parent().ok_or(CliWorkspaceErrorV1 {
72 code: "L9F001_CONFIGURATION_PATH_INVALID",
73 message: "configuration path must have a parent directory".into(),
74 })?;
75 let envelope =
76 load_explicit_project_envelope_v1(project_root, configuration_path).map_err(|error| {
77 CliWorkspaceErrorV1 {
78 code: error.code,
79 message: error.message,
80 }
81 })?;
82 let sources =
83 load_explicit_source_inputs_v1(&envelope.project_root, source_specs).map_err(|error| {
84 CliWorkspaceErrorV1 {
85 code: error.code,
86 message: error.message,
87 }
88 })?;
89 let contract = compiler_contract();
90 let workspace_id =
91 derive_workspace_id_v1(&envelope.configuration).map_err(|error| CliWorkspaceErrorV1 {
92 code: "L9F002_INVALID_CONFIGURATION",
93 message: error.message,
94 })?;
95 let snapshot = WorkspaceSnapshot::from_input(&WorkspaceInput {
96 configuration: envelope.configuration.clone(),
97 sources: sources
98 .iter()
99 .map(|source| WorkspaceSource {
100 path: source.logical_path.clone(),
101 source: source.content.clone(),
102 language: None,
103 })
104 .collect(),
105 compiler_contract: contract.clone(),
106 })
107 .map_err(|error| CliWorkspaceErrorV1 {
108 code: "L9F003_INVALID_COMPLETE_CANDIDATE",
109 message: error.message,
110 })?;
111 let mut service = CompilerServiceHost::start(envelope.project_root.join(".presolve"), contract)
112 .map_err(|error| CliWorkspaceErrorV1 {
113 code: error.code,
114 message: error.message,
115 })?;
116 let session_id = service
117 .open_session(envelope.configuration.clone(), &workspace_id)
118 .map_err(|error| CliWorkspaceErrorV1 {
119 code: error.code,
120 message: error.message,
121 })?;
122 let manifest = WorkspaceManifestV1 {
123 schema: WORKSPACE_MANIFEST_V1_SCHEMA.into(),
124 version: 1,
125 workspace_id: workspace_id.to_string(),
126 packages: vec![WorkspacePackageDescriptorV1 {
127 package_id: "project".into(),
128 session_id: session_id.clone(),
129 display_name: Some("project".into()),
130 configuration_identity_hint: None,
131 metadata: BTreeMap::new(),
132 }],
133 dependencies: Vec::new(),
134 policy: WorkspacePolicyV1 {
135 failure_mode: "fail_fast".into(),
136 execution_mode: "deterministic_serial".into(),
137 result_detail: "summary".into(),
138 },
139 };
140 let request = CompileRequest {
141 configuration: envelope.configuration,
142 candidate_snapshot: snapshot,
143 sources: sources
144 .into_iter()
145 .map(|source| CompleteSource {
146 path: source.logical_path,
147 source: source.content,
148 language: None,
149 })
150 .collect(),
151 mode: RequestedCompilationMode::Automatic,
152 incremental_report: IncrementalReportSelector::Summary,
153 verify_exact_equivalence: verify_clean_equivalence,
154 cache_report: CacheReportSelector::Summary,
155 };
156 let result = service
157 .compile_workspace_v1(WorkspaceCompileRequestV1 {
158 manifest,
159 packages: vec![WorkspacePackageCompileRequestV1 {
160 package_id: "project".into(),
161 expected_commit_sequence: 0,
162 request,
163 }],
164 operation_id: "presolve-cli-workspace-v1".into(),
165 })
166 .map_err(|error| CliWorkspaceErrorV1 {
167 code: error.code,
168 message: error.message,
169 })?;
170 service
171 .verify_workspace(&result.workspace_id)
172 .map_err(|error| CliWorkspaceErrorV1 {
173 code: error.code,
174 message: error.message,
175 })?;
176 service
177 .close_session(&session_id)
178 .map_err(|error| CliWorkspaceErrorV1 {
179 code: error.code,
180 message: error.message,
181 })?;
182 Ok(CliWorkspaceResultV1 {
183 workspace_id: result.workspace_id,
184 status: result.status,
185 manifest_identity: result.manifest_identity,
186 graph_identity: result.graph_identity,
187 plan_identity: result.plan_identity,
188 package_snapshot_id: result
189 .package_results
190 .into_iter()
191 .next()
192 .and_then(|item| item.snapshot_id),
193 })
194}
195
196pub fn run_explicit_watch_once_v1(
199 configuration_path: &Path,
200 source_specs: &[CliExplicitSourceSpecV1],
201) -> Result<String, CliWorkspaceErrorV1> {
202 let project_root = configuration_path.parent().ok_or(CliWorkspaceErrorV1 {
203 code: "L9F001_CONFIGURATION_PATH_INVALID",
204 message: "configuration path must have a parent directory".into(),
205 })?;
206 let envelope =
207 load_explicit_project_envelope_v1(project_root, configuration_path).map_err(|error| {
208 CliWorkspaceErrorV1 {
209 code: error.code,
210 message: error.message,
211 }
212 })?;
213 let sources =
214 load_explicit_source_inputs_v1(&envelope.project_root, source_specs).map_err(|error| {
215 CliWorkspaceErrorV1 {
216 code: error.code,
217 message: error.message,
218 }
219 })?;
220 let contract = compiler_contract();
221 let workspace_id =
222 derive_workspace_id_v1(&envelope.configuration).map_err(|error| CliWorkspaceErrorV1 {
223 code: "L9F002_INVALID_CONFIGURATION",
224 message: error.message,
225 })?;
226 let snapshot = WorkspaceSnapshot::from_input(&WorkspaceInput {
227 configuration: envelope.configuration.clone(),
228 sources: sources
229 .iter()
230 .map(|source| WorkspaceSource {
231 path: source.logical_path.clone(),
232 source: source.content.clone(),
233 language: None,
234 })
235 .collect(),
236 compiler_contract: contract.clone(),
237 })
238 .map_err(|error| CliWorkspaceErrorV1 {
239 code: "L9F003_INVALID_COMPLETE_CANDIDATE",
240 message: error.message,
241 })?;
242 let mut service = CompilerServiceHost::start(envelope.project_root.join(".presolve"), contract)
243 .map_err(|error| CliWorkspaceErrorV1 {
244 code: error.code,
245 message: error.message,
246 })?;
247 let session_id = service
248 .open_session(envelope.configuration.clone(), &workspace_id)
249 .map_err(|error| CliWorkspaceErrorV1 {
250 code: error.code,
251 message: error.message,
252 })?;
253 let watch_session_id = format!("cli-watch:{workspace_id}");
254 service
255 .create_watch_session(WatchSessionConfigurationV1 {
256 schema: WATCH_SESSION_CONFIGURATION_V1_SCHEMA.into(),
257 version: 1,
258 watch_session_id: watch_session_id.clone(),
259 workspace_id: workspace_id.to_string(),
260 debounce: WatchDebounceV1 {
261 quiet_period_milliseconds: 0,
262 maximum_delay_milliseconds: 0,
263 },
264 supersession_policy: "cancel_obsolete".into(),
265 event_detail: "summary".into(),
266 event_journal_capacity: 16,
267 })
268 .map_err(|error| CliWorkspaceErrorV1 {
269 code: error.code,
270 message: error.message,
271 })?;
272 let manifest = WorkspaceManifestV1 {
273 schema: WORKSPACE_MANIFEST_V1_SCHEMA.into(),
274 version: 1,
275 workspace_id: workspace_id.to_string(),
276 packages: vec![WorkspacePackageDescriptorV1 {
277 package_id: "project".into(),
278 session_id: session_id.clone(),
279 display_name: Some("project".into()),
280 configuration_identity_hint: None,
281 metadata: BTreeMap::new(),
282 }],
283 dependencies: Vec::new(),
284 policy: WorkspacePolicyV1 {
285 failure_mode: "fail_fast".into(),
286 execution_mode: "deterministic_serial".into(),
287 result_detail: "summary".into(),
288 },
289 };
290 service
291 .submit_watch_change_batch(
292 WatchChangeBatchV1 {
293 schema: WATCH_CHANGE_BATCH_V1_SCHEMA.into(),
294 version: 1,
295 watch_session_id: watch_session_id.clone(),
296 sequence: 1,
297 observed_changes: sources
298 .iter()
299 .map(|source| ObservedChangeV1 {
300 kind: "modified".into(),
301 logical_path: source.logical_path.clone(),
302 previous_logical_path: None,
303 })
304 .collect(),
305 candidate: WatchCandidateV1 {
306 candidate_id: "initial-complete-candidate".into(),
307 fingerprint_hint: None,
308 verify_exact_equivalence: false,
309 workspace_request: WorkspaceCompileRequestV1 {
310 manifest,
311 packages: vec![WorkspacePackageCompileRequestV1 {
312 package_id: "project".into(),
313 expected_commit_sequence: 0,
314 request: CompileRequest {
315 configuration: envelope.configuration,
316 candidate_snapshot: snapshot,
317 sources: sources
318 .into_iter()
319 .map(|source| CompleteSource {
320 path: source.logical_path,
321 source: source.content,
322 language: None,
323 })
324 .collect(),
325 mode: RequestedCompilationMode::Automatic,
326 incremental_report: IncrementalReportSelector::Summary,
327 verify_exact_equivalence: false,
328 cache_report: CacheReportSelector::Summary,
329 },
330 }],
331 operation_id: "presolve-cli-watch-once-v1".into(),
332 },
333 },
334 },
335 0,
336 )
337 .map_err(|error| CliWorkspaceErrorV1 {
338 code: error.code,
339 message: error.message,
340 })?;
341 let report = service
342 .run_watch_scheduler_turn(&watch_session_id, 0)
343 .map_err(|error| CliWorkspaceErrorV1 {
344 code: error.code,
345 message: error.message,
346 })?
347 .ok_or(CliWorkspaceErrorV1 {
348 code: "L9F004_WATCH_SCHEDULER_DID_NOT_RUN",
349 message: "L8 did not schedule candidate".into(),
350 })?;
351 service
352 .stop_watch_session(&watch_session_id)
353 .map_err(|error| CliWorkspaceErrorV1 {
354 code: error.code,
355 message: error.message,
356 })?;
357 service
358 .close_session(&session_id)
359 .map_err(|error| CliWorkspaceErrorV1 {
360 code: error.code,
361 message: error.message,
362 })?;
363 Ok(report.outcome)
364}
365
366#[cfg(test)]
367mod tests {
368 use std::fs;
369 use std::sync::atomic::{AtomicU64, Ordering};
370
371 use super::*;
372 use crate::parse_explicit_source_spec_v1;
373
374 static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
375
376 #[test]
377 fn l9f_delegates_one_explicit_package_to_l7_and_verifies_the_result() {
378 let root = std::env::temp_dir().join(format!(
379 "presolve-l9f-{}",
380 NEXT_ROOT.fetch_add(1, Ordering::Relaxed)
381 ));
382 fs::create_dir_all(root.join("src")).unwrap();
383 let config = root.join("presolve.json");
384 fs::write(
385 &config,
386 include_bytes!("../fixtures/configuration/minimum-cli-v1.json"),
387 )
388 .unwrap();
389 fs::write(root.join("src/main.ts"), "export const value = 1;\n").unwrap();
390 let result = run_explicit_workspace_v1(
391 &config,
392 &[parse_explicit_source_spec_v1("src/main.ts=src/main.ts").unwrap()],
393 true,
394 )
395 .unwrap();
396 assert_eq!(result.status, "succeeded");
397 assert!(result.package_snapshot_id.unwrap().starts_with("snapshot:"));
398 fs::remove_dir_all(root).unwrap();
399 }
400}