1#![allow(
8 clippy::missing_errors_doc,
9 clippy::missing_panics_doc,
10 clippy::needless_pass_by_value,
11 clippy::single_match_else,
12 clippy::too_many_lines
13)]
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt::Write as _;
17use std::fs::{self, File, OpenOptions};
18use std::io::Write;
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::sync::Arc;
22
23use sha2::{Digest as _, Sha256};
24
25use crate::persistent_cache::{
26 CacheInspectionReportV1, CacheKeyInputV1, CacheOutcomeV1, CacheReportSelector,
27 CacheTelemetryV1, CachedCompileResultV1, PersistentArtifactCacheV1,
28};
29use crate::platform::{
30 self, CacheLimits, CancellationToken, CanonicalReusableProductV1, CompilationOutcome,
31 CompileWorkspaceRequest, CompilerSessionState, ContractVersion, IncrementalCompilationModeV1,
32 IncrementalCompileWorkspaceRequestV1, IncrementalFallbackReasonV1, RequestedCompilationMode,
33 SourceRevisionId, SourceUnitId, WorkspaceConfiguration, WorkspaceGraph, WorkspaceId,
34 WorkspaceInput, WorkspaceSnapshot, WorkspaceSource,
35};
36use crate::watch::{self, WatchChangeBatchMetadataV1, WatchSessionV1};
37use crate::workspace::{self, WorkspaceBuildPlanV1, WorkspaceManifestV1, WorkspacePackageGraphV1};
38
39pub const COMPILER_SERVICE_PROTOCOL_VERSION: u32 = 1;
40pub const COMPILER_SERVICE_DESCRIPTOR_SCHEMA_VERSION: u32 = 1;
41pub const COMPILER_SERVICE_REQUEST_SCHEMA_VERSION: u32 = 1;
42pub const COMPILER_SERVICE_RESPONSE_SCHEMA_VERSION: u32 = 1;
43pub const DURABLE_SESSION_SCHEMA_VERSION: u32 = 1;
44pub const DURABLE_COMMIT_SCHEMA_VERSION: u32 = 1;
45pub const SESSION_JOURNAL_SCHEMA_VERSION: u32 = 1;
46pub const PERSISTENCE_MANIFEST_SCHEMA_VERSION: u32 = 1;
47pub const SERVICE_INSPECTION_SCHEMA_VERSION: u32 = 1;
48pub const SESSION_INSPECTION_SCHEMA_VERSION: u32 = 1;
49pub const MAXIMUM_FRAME_BYTES: usize = 16 * 1024 * 1024;
50
51static NEXT_SERVICE: AtomicU64 = AtomicU64::new(1);
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ServiceError {
55 pub code: &'static str,
56 pub message: String,
57 pub retryable: bool,
58}
59impl ServiceError {
60 fn new(code: &'static str, message: impl Into<String>) -> Self {
61 Self {
62 code,
63 message: message.into(),
64 retryable: false,
65 }
66 }
67}
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct CompilerServiceDescriptor {
70 pub schema_version: u32,
71 pub protocol_version: u32,
72 pub service_instance_id: String,
73 pub compiler_contract: ContractVersion,
74 pub platform_contract: String,
75 pub persistence_version: u32,
76 pub capabilities: Vec<String>,
77}
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ServiceOperation {
80 Handshake,
81 ServiceStatus,
82 OpenSession,
83 ResumeSession,
84 Compile,
85 Cancel,
86 SessionStatus,
87 CloseSession,
88 Shutdown,
89}
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ServiceResponseStatus {
92 Ok,
93 Rejected,
94 Cancelled,
95 Failed,
96}
97#[derive(Debug, Clone)]
98pub struct CompleteSource {
99 pub path: String,
100 pub source: String,
101 pub language: Option<platform::SourceLanguage>,
102}
103#[derive(Debug, Clone)]
104pub struct CompileRequest {
105 pub configuration: WorkspaceConfiguration,
106 pub candidate_snapshot: WorkspaceSnapshot,
107 pub sources: Vec<CompleteSource>,
108 pub mode: RequestedCompilationMode,
109 pub incremental_report: IncrementalReportSelector,
111 pub verify_exact_equivalence: bool,
113 pub cache_report: CacheReportSelector,
115}
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum IncrementalReportSelector {
118 None,
119 Summary,
120 Full,
121}
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct IncrementalExecutionReportV1 {
124 pub schema: &'static str,
125 pub plan_fingerprint: platform::Digest,
126 pub mode: IncrementalCompilationModeV1,
127 pub changed_inputs: Vec<platform::IncrementalInputChangeV1>,
128 pub invalidated_identities: Vec<SourceUnitId>,
129 pub reused_product_identities: Vec<platform::ProductKey>,
130 pub recomputed_work_unit_identities: Vec<SourceUnitId>,
131 pub fallback_reasons: Vec<IncrementalFallbackReasonV1>,
132 pub publication_outcome: &'static str,
133 pub exact_equivalence_verified: Option<bool>,
134}
135impl IncrementalExecutionReportV1 {
136 #[must_use]
141 pub fn to_canonical_json(&self) -> Vec<u8> {
142 let quote = |value: &str| serde_json::to_string(value).expect("strings serialize");
143 let changes = self
144 .changed_inputs
145 .iter()
146 .map(|change| {
147 format!(
148 "{{\"kind\":{},\"identity\":{}}}",
149 quote(change.kind.as_str()),
150 quote(&change.identity)
151 )
152 })
153 .collect::<Vec<_>>()
154 .join(",");
155 let ids = |values: &[SourceUnitId]| {
156 values
157 .iter()
158 .map(|value| quote(value.as_str()))
159 .collect::<Vec<_>>()
160 .join(",")
161 };
162 let reused = self
163 .reused_product_identities
164 .iter()
165 .map(|value| quote(value.as_str()))
166 .collect::<Vec<_>>()
167 .join(",");
168 let fallback = self
169 .fallback_reasons
170 .iter()
171 .map(|reason| quote(reason.code()))
172 .collect::<Vec<_>>()
173 .join(",");
174 format!("{{\"schema\":{},\"plan_fingerprint\":{},\"mode\":{},\"changed_inputs\":[{}],\"invalidated_identities\":[{}],\"reused_product_identities\":[{}],\"recomputed_work_unit_identities\":[{}],\"fallback_reasons\":[{}],\"publication_outcome\":{},\"exact_equivalence_verified\":{}}}\n",quote(self.schema),quote(self.plan_fingerprint.as_str()),quote(self.mode.as_str()),changes,ids(&self.invalidated_identities),reused,ids(&self.recomputed_work_unit_identities),fallback,quote(self.publication_outcome),self.exact_equivalence_verified.map_or_else(|| "null".into(), |value| value.to_string())).into_bytes()
175 }
176}
177#[derive(Debug, Clone)]
178pub struct CompileResponse {
179 pub commit_sequence: u64,
180 pub snapshot: Arc<WorkspaceSnapshot>,
181 pub graph: Arc<WorkspaceGraph>,
182 pub mode: String,
183 pub incremental_report: Option<IncrementalExecutionReportV1>,
184 pub cache_report: Option<CacheTelemetryV1>,
185}
186#[derive(Debug, Clone)]
187pub struct WorkspacePackageCompileRequestV1 {
188 pub package_id: String,
189 pub expected_commit_sequence: u64,
190 pub request: CompileRequest,
191}
192#[derive(Debug, Clone)]
193pub struct WorkspaceCompileRequestV1 {
194 pub manifest: WorkspaceManifestV1,
195 pub packages: Vec<WorkspacePackageCompileRequestV1>,
196 pub operation_id: String,
197}
198#[derive(Debug, Clone)]
199pub struct WorkspacePackageResultV1 {
200 pub package_id: String,
201 pub status: String,
202 pub snapshot_id: Option<String>,
203}
204#[derive(Debug, Clone)]
205pub struct WorkspaceBuildResultV1 {
206 pub workspace_id: String,
207 pub status: String,
208 pub manifest_identity: String,
209 pub graph_identity: String,
210 pub plan_identity: String,
211 pub package_results: Vec<WorkspacePackageResultV1>,
212}
213#[derive(Debug, Clone)]
216pub struct WatchCandidateV1 {
217 pub candidate_id: String,
218 pub fingerprint_hint: Option<String>,
219 pub verify_exact_equivalence: bool,
220 pub workspace_request: WorkspaceCompileRequestV1,
221}
222#[derive(Debug, Clone)]
223pub struct WatchChangeBatchV1 {
224 pub schema: String,
225 pub version: u32,
226 pub watch_session_id: String,
227 pub sequence: u64,
228 pub observed_changes: Vec<watch::ObservedChangeV1>,
229 pub candidate: WatchCandidateV1,
230}
231struct WatchHostSession {
232 state: WatchSessionV1,
233 pending: Option<WatchCandidateV1>,
236}
237#[derive(Debug, Clone)]
238struct DurableWorkspaceStateV1 {
239 manifest: WorkspaceManifestV1,
240 graph: WorkspacePackageGraphV1,
241 plan: WorkspaceBuildPlanV1,
242 result: WorkspaceBuildResultV1,
243}
244
245pub fn encode_frame(payload: &[u8]) -> Result<Vec<u8>, ServiceError> {
246 if payload.is_empty() {
247 return Err(ServiceError::new("invalid_frame", "empty payload"));
248 }
249 if payload.len() > MAXIMUM_FRAME_BYTES {
250 return Err(ServiceError::new(
251 "frame_too_large",
252 "payload exceeds service limit",
253 ));
254 }
255 let mut frame = format!("{:08x}:", payload.len()).into_bytes();
256 frame.extend_from_slice(payload);
257 frame.push(b'\n');
258 Ok(frame)
259}
260pub fn decode_frame(frame: &[u8]) -> Result<Vec<u8>, ServiceError> {
261 if frame.len() < 10 || frame[8] != b':' || *frame.last().unwrap_or(&0) != b'\n' {
262 return Err(ServiceError::new("invalid_frame", "malformed frame"));
263 }
264 let header = std::str::from_utf8(&frame[..8])
265 .map_err(|_| ServiceError::new("invalid_frame", "non-ASCII length"))?;
266 if !header
267 .bytes()
268 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
269 {
270 return Err(ServiceError::new(
271 "invalid_frame",
272 "length is not lowercase hexadecimal",
273 ));
274 }
275 let length = usize::from_str_radix(header, 16)
276 .map_err(|_| ServiceError::new("invalid_frame", "invalid length"))?;
277 if length == 0 || length > MAXIMUM_FRAME_BYTES || frame.len() != length + 10 {
278 return Err(ServiceError::new(
279 "invalid_frame",
280 "truncated or trailing frame",
281 ));
282 }
283 Ok(frame[9..frame.len() - 1].to_vec())
284}
285
286pub struct CompilerServiceHost {
287 root: PathBuf,
288 descriptor: CompilerServiceDescriptor,
289 sessions: BTreeMap<String, DurableSession>,
290 cache: PersistentArtifactCacheV1,
291 workspaces: BTreeMap<String, DurableWorkspaceStateV1>,
292 watch_sessions: BTreeMap<String, WatchHostSession>,
293}
294struct DurableSession {
295 workspace_id: WorkspaceId,
296 configuration: WorkspaceConfiguration,
297 compiler_contract: ContractVersion,
298 commit_sequence: u64,
299 closed: bool,
300 l3: CompilerSessionState,
301 incremental_baseline: Option<IncrementalBaseline>,
302}
303#[derive(Clone)]
304struct IncrementalBaseline {
305 publication_identity: String,
306 configuration: WorkspaceConfiguration,
307 source_fingerprints: BTreeMap<SourceUnitId, SourceRevisionId>,
308 snapshot: Arc<WorkspaceSnapshot>,
309 graph: Arc<WorkspaceGraph>,
310 reusable_products: Vec<CanonicalReusableProductV1>,
311 compiler_contract: ContractVersion,
312}
313impl IncrementalBaseline {
314 fn is_consistent(&self) -> bool {
315 self.publication_identity == self.snapshot.snapshot_id.as_str()
316 && self.compiler_contract == self.snapshot.compiler_contract
317 && platform::workspace_configuration_fingerprint_v1(&self.configuration)
318 .is_ok_and(|fingerprint| fingerprint == self.snapshot.configuration_fingerprint)
319 && self.source_fingerprints
320 == self
321 .snapshot
322 .units
323 .iter()
324 .map(|unit| (unit.source_unit_id.clone(), unit.source_revision_id.clone()))
325 .collect()
326 }
327}
328impl CompilerServiceHost {
329 pub fn start(
330 root: impl AsRef<Path>,
331 compiler_contract: ContractVersion,
332 ) -> Result<Self, ServiceError> {
333 Self::start_with_cache(root, None, compiler_contract)
334 }
335 pub fn start_with_cache(
336 root: impl AsRef<Path>,
337 cache_root: Option<&Path>,
338 compiler_contract: ContractVersion,
339 ) -> Result<Self, ServiceError> {
340 let root = root.as_ref().join("service");
341 fs::create_dir_all(root.join("sessions")).map_err(io_error)?;
342 let service_id = format!(
343 "service-instance:{:032x}",
344 NEXT_SERVICE.fetch_add(1, Ordering::Relaxed)
345 );
346 let descriptor = CompilerServiceDescriptor {
347 schema_version: 1,
348 protocol_version: 1,
349 service_instance_id: service_id,
350 compiler_contract: compiler_contract.clone(),
351 platform_contract: "presolve-platform-l3:1".into(),
352 persistence_version: 1,
353 capabilities: vec![
354 "canonical_inspection".into(),
355 "durable_sessions".into(),
356 "incremental_compile".into(),
357 "request_cancellation".into(),
358 "session_recovery".into(),
359 "explicit_watch_sessions".into(),
360 ],
361 };
362 let host = Self {
363 root,
364 descriptor,
365 sessions: BTreeMap::new(),
366 cache: PersistentArtifactCacheV1::open(cache_root, &compiler_contract),
367 workspaces: BTreeMap::new(),
368 watch_sessions: BTreeMap::new(),
369 };
370 host.write_manifest()?;
371 Ok(host)
372 }
373 #[must_use]
374 pub fn descriptor(&self) -> &CompilerServiceDescriptor {
375 &self.descriptor
376 }
377 pub fn open_session(
378 &mut self,
379 configuration: WorkspaceConfiguration,
380 claimed: &WorkspaceId,
381 ) -> Result<String, ServiceError> {
382 let workspace = platform::derive_workspace_id_v1(&configuration).map_err(platform_error)?;
383 if &workspace != claimed {
384 return Err(ServiceError::new(
385 "workspace_mismatch",
386 "workspace identity does not match configuration",
387 ));
388 }
389 let id = format!(
390 "session:sha256:{:x}",
391 Sha256::digest(
392 format!(
393 "presolve-durable-session-v1\0{}\0{}",
394 workspace.as_str(),
395 NEXT_SERVICE.fetch_add(1, Ordering::Relaxed)
396 )
397 .as_bytes()
398 )
399 );
400 let l3 = CompilerSessionState::new(
401 workspace.clone(),
402 self.descriptor.compiler_contract.clone(),
403 CacheLimits::default(),
404 );
405 let session = DurableSession {
406 workspace_id: workspace,
407 configuration,
408 compiler_contract: self.descriptor.compiler_contract.clone(),
409 commit_sequence: 0,
410 closed: false,
411 l3,
412 incremental_baseline: None,
413 };
414 self.write_session(&id, &session)?;
415 append_journal(&self.root, &id, 1, "session_created", 0, None)?;
416 self.sessions.insert(id.clone(), session);
417 Ok(id)
418 }
419 pub fn close_session(&mut self, session_id: &str) -> Result<(), ServiceError> {
422 let root = self.root.clone();
423 let session = self
424 .sessions
425 .get_mut(session_id)
426 .ok_or_else(|| ServiceError::new("session_not_found", "session not loaded"))?;
427 if session.closed {
428 return Ok(());
429 }
430 session.incremental_baseline = None;
431 session.l3.close();
432 session.closed = true;
433 write_session_at(&root, session_id, session)?;
434 append_journal(
435 &root,
436 session_id,
437 session.commit_sequence.saturating_mul(2).saturating_add(2),
438 "session_closed",
439 session.commit_sequence,
440 None,
441 )
442 }
443 pub fn verify_cache(
444 &self,
445 _explicit_root: &Path,
446 ) -> Result<CacheInspectionReportV1, ServiceError> {
447 self.cache.verify().map_err(cache_operation_error)
448 }
449 pub fn inspect_cache(
450 &self,
451 _explicit_root: &Path,
452 ) -> Result<CacheInspectionReportV1, ServiceError> {
453 self.cache.inspect().map_err(cache_operation_error)
454 }
455 pub fn clean_cache(&self, _explicit_root: &Path) -> Result<Vec<String>, ServiceError> {
456 self.cache.clean().map_err(cache_operation_error)
457 }
458 pub fn create_watch_session(
461 &mut self,
462 configuration: watch::WatchSessionConfigurationV1,
463 ) -> Result<watch::WatchSessionSnapshotV1, ServiceError> {
464 let id = configuration.watch_session_id.clone();
465 if self.watch_sessions.contains_key(&id) {
466 return Err(ServiceError::new(
467 "L8W001_WATCH_SESSION_ALREADY_EXISTS",
468 "watch session already exists",
469 ));
470 }
471 if self.watch_sessions.len() >= watch::MAX_WATCH_SESSIONS {
472 return Err(ServiceError::new(
473 "L8W010_WATCH_RESOURCE_LIMIT_EXCEEDED",
474 "maximum process-local watch sessions reached",
475 ));
476 }
477 let state = WatchSessionV1::new(configuration).map_err(watch_error)?;
478 let snapshot = state.snapshot();
479 self.watch_sessions.insert(
480 id,
481 WatchHostSession {
482 state,
483 pending: None,
484 },
485 );
486 Ok(snapshot)
487 }
488 pub fn submit_watch_change_batch(
491 &mut self,
492 batch: WatchChangeBatchV1,
493 monotonic_milliseconds: u64,
494 ) -> Result<watch::WatchAcceptanceV1, ServiceError> {
495 let workspace_id = batch
496 .candidate
497 .workspace_request
498 .manifest
499 .normalize_validate()
500 .map_err(workspace_error)?
501 .workspace_id;
502 let session = self
503 .watch_sessions
504 .get_mut(&batch.watch_session_id)
505 .ok_or_else(|| {
506 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
507 })?;
508 if session.state.configuration().workspace_id != workspace_id {
509 return Err(ServiceError::new(
510 "L8W003_WATCH_WORKSPACE_MISMATCH",
511 "candidate workspace does not match watch session",
512 ));
513 }
514 let fingerprint = watch_candidate_fingerprint(&batch.candidate.workspace_request)?;
515 if batch
516 .candidate
517 .fingerprint_hint
518 .as_ref()
519 .is_some_and(|hint| hint != &fingerprint)
520 {
521 return Err(ServiceError::new(
522 "L8W008_CANDIDATE_IDENTITY_HINT_MISMATCH",
523 "candidate fingerprint hint does not match the derived identity",
524 ));
525 }
526 let metadata = WatchChangeBatchMetadataV1 {
527 schema: batch.schema,
528 version: batch.version,
529 watch_session_id: batch.watch_session_id,
530 sequence: batch.sequence,
531 observed_changes: batch.observed_changes,
532 candidate: watch::WatchCandidateMetadataV1 {
533 candidate_id: batch.candidate.candidate_id.clone(),
534 fingerprint,
535 },
536 };
537 let acceptance = session
538 .state
539 .accept(metadata, monotonic_milliseconds)
540 .map_err(watch_error)?;
541 if acceptance.retained_pending {
542 session.pending = Some(batch.candidate);
545 }
546 Ok(acceptance)
547 }
548 pub fn flush_watch_session(&mut self, watch_session_id: &str) -> Result<(), ServiceError> {
549 self.watch_sessions
550 .get_mut(watch_session_id)
551 .ok_or_else(|| {
552 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
553 })?
554 .state
555 .flush()
556 .map_err(watch_error)
557 }
558 pub fn run_watch_scheduler_turn(
561 &mut self,
562 watch_session_id: &str,
563 monotonic_milliseconds: u64,
564 ) -> Result<Option<watch::WatchExecutionReportV1>, ServiceError> {
565 let (plan, candidate) = {
566 let session = self
567 .watch_sessions
568 .get_mut(watch_session_id)
569 .ok_or_else(|| {
570 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
571 })?;
572 let Some(plan) = session.state.scheduler_turn(monotonic_milliseconds) else {
573 return Ok(None);
574 };
575 let candidate = session.pending.take().ok_or_else(|| {
576 ServiceError::new(
577 "L8W007_INVALID_CHANGE_BATCH",
578 "missing transient pending candidate",
579 )
580 })?;
581 (plan, candidate)
582 };
583 let outcome = self.compile_workspace_v1(candidate.workspace_request);
586 let (success, identity) = match &outcome {
587 Ok(result) if result.status == "succeeded" => {
588 (true, Some(workspace_result_identity(result)))
589 }
590 _ => (false, None),
591 };
592 let report = self
593 .watch_sessions
594 .get_mut(watch_session_id)
595 .expect("session cannot be removed during serialized turn")
596 .state
597 .finish(success, identity);
598 debug_assert!(report.as_ref().is_some_and(|report| report.plan == plan));
599 match outcome {
600 Ok(_) => Ok(report),
601 Err(error) => {
604 if report.is_some() {
605 Ok(report)
606 } else {
607 Err(error)
608 }
609 }
610 }
611 }
612 pub fn poll_watch_events(
613 &self,
614 watch_session_id: &str,
615 after_event_sequence: u64,
616 ) -> Result<Vec<watch::WatchEventV1>, ServiceError> {
617 self.watch_sessions
618 .get(watch_session_id)
619 .ok_or_else(|| {
620 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
621 })?
622 .state
623 .poll(after_event_sequence)
624 .map_err(watch_error)
625 }
626 pub fn inspect_watch_session(
627 &self,
628 watch_session_id: &str,
629 ) -> Result<watch::WatchSessionSnapshotV1, ServiceError> {
630 self.watch_sessions
631 .get(watch_session_id)
632 .map(|session| session.state.snapshot())
633 .ok_or_else(|| {
634 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
635 })
636 }
637 pub fn stop_watch_session(
638 &mut self,
639 watch_session_id: &str,
640 ) -> Result<watch::WatchSessionSnapshotV1, ServiceError> {
641 let session = self
642 .watch_sessions
643 .get_mut(watch_session_id)
644 .ok_or_else(|| {
645 ServiceError::new("L8W002_WATCH_SESSION_NOT_FOUND", "watch session not found")
646 })?;
647 session.pending = None;
648 session.state.stop();
649 Ok(session.state.snapshot())
650 }
651 pub fn compile_workspace_v1(
652 &mut self,
653 request: WorkspaceCompileRequestV1,
654 ) -> Result<WorkspaceBuildResultV1, ServiceError> {
655 let manifest = request
656 .manifest
657 .normalize_validate()
658 .map_err(workspace_error)?;
659 let graph = workspace::graph(&manifest).map_err(workspace_error)?;
660 let expected = manifest
661 .packages
662 .iter()
663 .map(|p| p.package_id.clone())
664 .collect::<BTreeSet<_>>();
665 let supplied = request
666 .packages
667 .iter()
668 .map(|p| p.package_id.clone())
669 .collect::<BTreeSet<_>>();
670 if expected != supplied || supplied.len() != request.packages.len() {
671 return Err(ServiceError::new(
672 "L7W007_PACKAGE_REQUEST_SET_MISMATCH",
673 "workspace request keys do not match manifest",
674 ));
675 }
676 let mut requests = request
677 .packages
678 .into_iter()
679 .map(|p| (p.package_id.clone(), p))
680 .collect::<BTreeMap<_, _>>();
681 for package in &manifest.packages {
682 let item = requests
683 .get(&package.package_id)
684 .expect("validated request");
685 if package.session_id != item.request.candidate_snapshot.workspace_id.as_str()
686 && !self.sessions.contains_key(&package.session_id)
687 {
688 return Err(ServiceError::new(
689 "L7W011_WORKSPACE_SESSION_OWNERSHIP_CONFLICT",
690 "package session does not exist",
691 ));
692 }
693 if let Some(hint) = &package.configuration_identity_hint {
694 let actual =
695 platform::workspace_configuration_fingerprint_v1(&item.request.configuration)
696 .map_err(platform_error)?;
697 if hint != actual.as_str() {
698 return Err(ServiceError::new(
699 "L7W008_CONFIGURATION_IDENTITY_HINT_MISMATCH",
700 "configuration identity hint mismatch",
701 ));
702 }
703 }
704 }
705 let fingerprints = requests
706 .iter()
707 .map(|(id, item)| {
708 (
709 id.clone(),
710 item.request
711 .candidate_snapshot
712 .snapshot_id
713 .as_str()
714 .to_owned(),
715 )
716 })
717 .collect();
718 let plan = workspace::plan(&graph, fingerprints);
719 let mut results = Vec::new();
720 let mut failed = false;
721 for stage in &plan.stages {
722 for package_id in &stage.packages {
723 let item = requests.remove(package_id).expect("plan package");
724 let session_id = manifest
725 .packages
726 .iter()
727 .find(|p| p.package_id == *package_id)
728 .expect("descriptor")
729 .session_id
730 .clone();
731 if failed {
732 results.push(WorkspacePackageResultV1 {
733 package_id: package_id.clone(),
734 status: "skipped_fail_fast".into(),
735 snapshot_id: None,
736 });
737 continue;
738 }
739 match self.compile(&session_id, item.expected_commit_sequence, item.request) {
740 Ok(response) => results.push(WorkspacePackageResultV1 {
741 package_id: package_id.clone(),
742 status: "succeeded".into(),
743 snapshot_id: Some(response.snapshot.snapshot_id.to_string()),
744 }),
745 Err(_) => {
746 failed = true;
747 results.push(WorkspacePackageResultV1 {
748 package_id: package_id.clone(),
749 status: "failed".into(),
750 snapshot_id: None,
751 });
752 }
753 }
754 }
755 }
756 let status = if failed { "failed" } else { "succeeded" }.to_owned();
757 let result = WorkspaceBuildResultV1 {
758 workspace_id: manifest.workspace_id.clone(),
759 status,
760 manifest_identity: graph.manifest_identity.clone(),
761 graph_identity: graph.graph_identity.clone(),
762 plan_identity: plan.plan_identity.clone(),
763 package_results: results,
764 };
765 if !failed {
766 let state = DurableWorkspaceStateV1 {
767 manifest: manifest.clone(),
768 graph: graph.clone(),
769 plan: plan.clone(),
770 result: result.clone(),
771 };
772 self.publish_workspace_state(&state)?;
773 self.workspaces.insert(manifest.workspace_id.clone(), state);
774 }
775 Ok(result)
776 }
777 pub fn inspect_workspace(
778 &self,
779 workspace_id: &str,
780 ) -> Result<WorkspaceBuildResultV1, ServiceError> {
781 self.workspaces
782 .get(workspace_id)
783 .map(|state| state.result.clone())
784 .ok_or_else(|| ServiceError::new("workspace_not_found", "workspace state not found"))
785 }
786 pub fn verify_workspace(&self, workspace_id: &str) -> Result<(), ServiceError> {
787 let state = self
788 .workspaces
789 .get(workspace_id)
790 .ok_or_else(|| ServiceError::new("workspace_not_found", "workspace state not found"))?;
791 if state.graph.manifest_identity != state.manifest.identity()
792 || state.plan.manifest_identity != state.graph.manifest_identity
793 {
794 return Err(ServiceError::new(
795 "workspace_state_invalid",
796 "workspace state integrity mismatch",
797 ));
798 }
799 Ok(())
800 }
801 pub fn remove_workspace_state(&mut self, workspace_id: &str) -> Result<(), ServiceError> {
802 self.workspaces.remove(workspace_id);
803 let path = self
804 .root
805 .join("workspaces")
806 .join(format!("{workspace_id}.json"));
807 if path.exists() {
808 fs::remove_file(path).map_err(io_error)?;
809 }
810 Ok(())
811 }
812 pub fn compile(
813 &mut self,
814 session_id: &str,
815 expected: u64,
816 request: CompileRequest,
817 ) -> Result<CompileResponse, ServiceError> {
818 let root = self.root.clone();
819 let session = self
820 .sessions
821 .get_mut(session_id)
822 .ok_or_else(|| ServiceError::new("session_not_found", "session not loaded"))?;
823 if session.closed {
824 return Err(ServiceError::new("session_closed", "session is closed"));
825 }
826 if expected != session.commit_sequence {
827 return Err(ServiceError::new(
828 "stale_session_state",
829 "commit sequence is stale",
830 ));
831 }
832 platform::canonical_workspace_configuration_json_v1(&request.configuration)
833 .map_err(|error| ServiceError::new("invalid_request", error.message))?;
834 let workspace = WorkspaceInput {
835 configuration: request.configuration.clone(),
836 sources: request
837 .sources
838 .iter()
839 .map(|source| WorkspaceSource {
840 path: source.path.clone(),
841 source: source.source.clone(),
842 language: source.language,
843 })
844 .collect(),
845 compiler_contract: session.compiler_contract.clone(),
846 };
847 let derived = WorkspaceSnapshot::from_input(&workspace).map_err(platform_failure)?;
848 if derived
849 .to_canonical_json()
850 .map_err(|error| ServiceError::new("invalid_request", error.message))?
851 != request
852 .candidate_snapshot
853 .to_canonical_json()
854 .map_err(|error| ServiceError::new("invalid_request", error.message))?
855 {
856 return Err(ServiceError::new(
857 "source_revision_mismatch",
858 "complete source input does not match candidate snapshot",
859 ));
860 }
861 let raw_baseline = session.incremental_baseline.as_ref();
862 let baseline_is_malformed = raw_baseline.is_some_and(|baseline| !baseline.is_consistent());
863 let baseline = raw_baseline.filter(|_| !baseline_is_malformed);
864 let mut plan = platform::plan_incremental_compilation_v1(
865 baseline.map(|value| (value.snapshot.as_ref(), value.graph.as_ref())),
866 &derived,
867 );
868 if baseline_is_malformed {
869 platform::force_clean_fallback_v1(
870 &mut plan,
871 &derived,
872 IncrementalFallbackReasonV1::MalformedBaselineGraph,
873 );
874 }
875 let cache_input = CacheKeyInputV1 {
876 compiler_contract: session.compiler_contract.clone(),
877 configuration_fingerprint: derived.configuration_fingerprint.as_str().into(),
878 source_universe_fingerprint: platform::source_universe_fingerprint_v1(&derived)
879 .as_str()
880 .into(),
881 compile_mode: match request.mode {
882 RequestedCompilationMode::Automatic => "automatic",
883 RequestedCompilationMode::Full => "full",
884 },
885 };
886 if plan.mode == IncrementalCompilationModeV1::NoChange {
887 let Some(baseline) = baseline else {
888 return Err(ServiceError::new(
889 "internal_invariant_failed",
890 "L5 no-change plan has no baseline",
891 ));
892 };
893 baseline.snapshot.validate().map_err(platform_failure)?;
894 baseline.graph.validate().map_err(platform_failure)?;
895 let report = selected_report(
896 request.incremental_report,
897 &plan,
898 Vec::new(),
899 Vec::new(),
900 "published",
901 Some(true),
902 );
903 return Ok(CompileResponse {
904 commit_sequence: session.commit_sequence,
905 snapshot: Arc::clone(&baseline.snapshot),
906 graph: Arc::clone(&baseline.graph),
907 mode: plan.mode.as_str().into(),
908 incremental_report: report,
909 cache_report: selected_cache_report(
910 request.cache_report,
911 CacheTelemetryV1 {
912 enabled: self.cache.enabled(),
913 outcome: CacheOutcomeV1::NotChecked,
914 reasons: Vec::new(),
915 cache_key: cache_input.key(),
916 payload_length: None,
917 result_fingerprint: None,
918 entry_published: false,
919 entry_replaced: false,
920 },
921 ),
922 });
923 }
924 if plan.mode == IncrementalCompilationModeV1::Incremental
927 && baseline.is_some_and(|value| value.reusable_products.is_empty())
928 {
929 platform::force_clean_fallback_v1(
930 &mut plan,
931 &derived,
932 IncrementalFallbackReasonV1::ReuseProductRejected,
933 );
934 }
935 let (cache_hit, mut cache_telemetry) = self.cache.lookup(&cache_input);
936 if let Some(hit) = cache_hit {
937 let previous_configuration = session.configuration.clone();
938 let previous_workspace_id = session.workspace_id.clone();
939 let previous_sequence = session.commit_sequence;
940 session.configuration = request.configuration.clone();
941 session.workspace_id = derived.workspace_id.clone();
942 session.commit_sequence += 1;
943 if publish_commit(
944 &root,
945 session_id,
946 session,
947 session.commit_sequence,
948 &hit.snapshot,
949 &hit.graph,
950 )
951 .is_ok()
952 {
953 session.incremental_baseline = Some(baseline_from_result(
954 &session.configuration,
955 Arc::new(hit.snapshot.clone()),
956 Arc::new(hit.graph.clone()),
957 Vec::new(),
958 &session.compiler_contract,
959 ));
960 return Ok(CompileResponse {
961 commit_sequence: session.commit_sequence,
962 snapshot: Arc::new(hit.snapshot),
963 graph: Arc::new(hit.graph),
964 mode: hit.response_mode,
965 incremental_report: selected_report(
966 request.incremental_report,
967 &plan,
968 Vec::new(),
969 Vec::new(),
970 "published",
971 None,
972 ),
973 cache_report: selected_cache_report(request.cache_report, cache_telemetry),
974 });
975 }
976 session.configuration = previous_configuration;
977 session.workspace_id = previous_workspace_id;
978 session.commit_sequence = previous_sequence;
979 cache_telemetry.outcome = CacheOutcomeV1::Miss;
980 cache_telemetry.reasons.push(
981 crate::persistent_cache::CacheReasonCodeV1::CanonicalProductValidationFailure,
982 );
983 }
984 let verification_workspace = request.verify_exact_equivalence.then(|| workspace.clone());
985 let (outcome, reused, recomputed) =
986 if plan.mode == IncrementalCompilationModeV1::Incremental {
987 let reusable_products = baseline
988 .map(|value| value.reusable_products.clone())
989 .unwrap_or_default();
990 let execution = session.l3.compile_workspace_incremental_v1(
991 IncrementalCompileWorkspaceRequestV1 {
992 workspace,
993 cancellation: CancellationToken::new(),
994 plan: plan.clone(),
995 reusable_products,
996 },
997 );
998 (
999 execution.outcome,
1000 execution.reused_product_identities,
1001 execution.recomputed_work_units,
1002 )
1003 } else {
1004 let outcome = session.l3.compile_workspace(CompileWorkspaceRequest {
1005 workspace,
1006 mode: RequestedCompilationMode::Full,
1008 cancellation: CancellationToken::new(),
1009 });
1010 (outcome, Vec::new(), plan.recompute_work_units.clone())
1011 };
1012 let CompilationOutcome::Committed(committed) = outcome else {
1013 return Err(ServiceError::new(
1014 "compiler_platform_failed",
1015 "compiler did not commit",
1016 ));
1017 };
1018 let equivalence = if let Some(clean_workspace) = verification_workspace {
1019 let mut clean = CompilerSessionState::new(
1020 derived.workspace_id.clone(),
1021 session.compiler_contract.clone(),
1022 CacheLimits::default(),
1023 );
1024 let clean = clean.compile_workspace(CompileWorkspaceRequest {
1025 workspace: clean_workspace,
1026 mode: RequestedCompilationMode::Full,
1027 cancellation: CancellationToken::new(),
1028 });
1029 let CompilationOutcome::Committed(clean) = clean else {
1030 return Err(ServiceError::new(
1031 "incremental_equivalence_failed",
1032 "isolated clean L3 compilation failed",
1033 ));
1034 };
1035 if clean
1036 .snapshot
1037 .to_canonical_json()
1038 .map_err(platform_serialization)?
1039 != committed
1040 .snapshot
1041 .to_canonical_json()
1042 .map_err(platform_serialization)?
1043 || clean
1044 .graph
1045 .to_canonical_json()
1046 .map_err(platform_serialization)?
1047 != committed
1048 .graph
1049 .to_canonical_json()
1050 .map_err(platform_serialization)?
1051 {
1052 return Err(ServiceError::new(
1053 "incremental_equivalence_failed",
1054 "first canonical mismatch is workspace snapshot or graph",
1055 ));
1056 }
1057 Some(true)
1058 } else {
1059 None
1060 };
1061 let previous_configuration = session.configuration.clone();
1062 let previous_workspace_id = session.workspace_id.clone();
1063 let previous_sequence = session.commit_sequence;
1064 session.configuration = request.configuration;
1065 session.workspace_id = derived.workspace_id.clone();
1066 session.commit_sequence += 1;
1067 if let Err(error) = publish_commit(
1068 &root,
1069 session_id,
1070 session,
1071 session.commit_sequence,
1072 &committed.snapshot,
1073 &committed.graph,
1074 ) {
1075 session.configuration = previous_configuration;
1076 session.workspace_id = previous_workspace_id;
1077 session.commit_sequence = previous_sequence;
1078 return Err(error);
1079 }
1080 session.incremental_baseline = Some(baseline_from_result(
1081 &session.configuration,
1082 Arc::clone(&committed.snapshot),
1083 Arc::clone(&committed.graph),
1084 committed.reusable_products,
1085 &session.compiler_contract,
1086 ));
1087 let cache_telemetry = self.cache.publish(
1088 &cache_input,
1089 &CachedCompileResultV1 {
1090 snapshot: (*committed.snapshot).clone(),
1091 graph: (*committed.graph).clone(),
1092 response_mode: plan.mode.as_str().into(),
1093 },
1094 );
1095 Ok(CompileResponse {
1096 commit_sequence: session.commit_sequence,
1097 snapshot: committed.snapshot,
1098 graph: committed.graph,
1099 mode: plan.mode.as_str().into(),
1100 incremental_report: selected_report(
1101 request.incremental_report,
1102 &plan,
1103 reused,
1104 recomputed,
1105 "published",
1106 equivalence,
1107 ),
1108 cache_report: selected_cache_report(request.cache_report, cache_telemetry),
1109 })
1110 }
1111 fn write_manifest(&self) -> Result<(), ServiceError> {
1112 atomic_write(&self.root.join("manifest.json"),format!("{{\"schema_version\":1,\"protocol_version\":1,\"persistence_version\":1,\"compiler_contract\":{},\"platform_contract\":\"presolve-platform-l3:1\"}}\n",json(&self.descriptor.compiler_contract.to_string())).as_bytes())
1113 }
1114 fn write_session(&self, id: &str, session: &DurableSession) -> Result<(), ServiceError> {
1115 let config = platform::canonical_workspace_configuration_json_v1(&session.configuration)
1116 .map_err(|error| ServiceError::new("persistence_io_failed", error.message))?;
1117 let config = String::from_utf8(config).map_err(|_| {
1118 ServiceError::new(
1119 "internal_invariant_failed",
1120 "non-UTF8 canonical configuration",
1121 )
1122 })?;
1123 let config = config.trim_end();
1124 let dir = self.session_dir(id);
1125 fs::create_dir_all(dir.join("commits")).map_err(io_error)?;
1126 atomic_write(&dir.join("session.json"),format!("{{\"schema_version\":1,\"session_id\":{},\"workspace_id\":{},\"workspace_configuration\":{},\"compiler_contract\":{},\"state\":{},\"current_commit_sequence\":{}}}\n",json(id),json(session.workspace_id.as_str()),config,json(session.compiler_contract.as_str()),json(if session.closed{"closed"}else{"open"}),session.commit_sequence).as_bytes())
1127 }
1128 fn publish_workspace_state(&self, state: &DurableWorkspaceStateV1) -> Result<(), ServiceError> {
1129 let directory = self.root.join("workspaces");
1130 fs::create_dir_all(&directory).map_err(io_error)?;
1131 let packages = state
1132 .result
1133 .package_results
1134 .iter()
1135 .map(|p| {
1136 format!(
1137 "{{\"package_id\":{},\"status\":{},\"snapshot_id\":{}}}",
1138 json(&p.package_id),
1139 json(&p.status),
1140 p.snapshot_id
1141 .as_ref()
1142 .map_or_else(|| "null".into(), |v| json(v))
1143 )
1144 })
1145 .collect::<Vec<_>>()
1146 .join(",");
1147 let bytes=format!("{{\"schema\":\"presolve.durable-workspace-state\",\"version\":1,\"workspace_id\":{},\"manifest_identity\":{},\"graph_identity\":{},\"plan_identity\":{},\"package_results\":[{}]}}\n",json(&state.result.workspace_id),json(&state.result.manifest_identity),json(&state.result.graph_identity),json(&state.result.plan_identity),packages);
1148 atomic_write(
1149 &directory.join(format!("{}.json", state.result.workspace_id)),
1150 bytes.as_bytes(),
1151 )
1152 }
1153}
1154fn selected_report(
1155 selector: IncrementalReportSelector,
1156 plan: &platform::IncrementalCompilationPlanV1,
1157 reused_product_identities: Vec<platform::ProductKey>,
1158 recomputed_work_unit_identities: Vec<SourceUnitId>,
1159 publication_outcome: &'static str,
1160 exact_equivalence_verified: Option<bool>,
1161) -> Option<IncrementalExecutionReportV1> {
1162 if selector == IncrementalReportSelector::None {
1163 return None;
1164 }
1165 let mut report = IncrementalExecutionReportV1 {
1166 schema: platform::INCREMENTAL_EXECUTION_REPORT_V1_SCHEMA,
1167 plan_fingerprint: plan.plan_fingerprint.clone(),
1168 mode: plan.mode,
1169 changed_inputs: plan.input_changes.clone(),
1170 invalidated_identities: plan.invalidation_closure.clone(),
1171 reused_product_identities,
1172 recomputed_work_unit_identities,
1173 fallback_reasons: plan.fallback_reasons.clone(),
1174 publication_outcome,
1175 exact_equivalence_verified,
1176 };
1177 if selector == IncrementalReportSelector::Summary {
1178 report.changed_inputs.clear();
1179 report.invalidated_identities.clear();
1180 report.reused_product_identities.clear();
1181 report.recomputed_work_unit_identities.clear();
1182 report.fallback_reasons.clear();
1183 }
1184 Some(report)
1185}
1186fn selected_cache_report(
1187 selector: CacheReportSelector,
1188 mut telemetry: CacheTelemetryV1,
1189) -> Option<CacheTelemetryV1> {
1190 if selector == CacheReportSelector::None {
1191 return None;
1192 }
1193 if selector == CacheReportSelector::Summary {
1194 telemetry.payload_length = None;
1195 telemetry.result_fingerprint = None;
1196 telemetry.entry_published = false;
1197 telemetry.entry_replaced = false;
1198 }
1199 Some(telemetry)
1200}
1201fn baseline_from_result(
1202 configuration: &WorkspaceConfiguration,
1203 snapshot: Arc<WorkspaceSnapshot>,
1204 graph: Arc<WorkspaceGraph>,
1205 reusable_products: Vec<CanonicalReusableProductV1>,
1206 compiler_contract: &ContractVersion,
1207) -> IncrementalBaseline {
1208 IncrementalBaseline {
1209 publication_identity: snapshot.snapshot_id.to_string(),
1210 configuration: configuration.clone(),
1211 source_fingerprints: snapshot
1212 .units
1213 .iter()
1214 .map(|unit| (unit.source_unit_id.clone(), unit.source_revision_id.clone()))
1215 .collect(),
1216 snapshot,
1217 graph,
1218 reusable_products,
1219 compiler_contract: compiler_contract.clone(),
1220 }
1221}
1222fn publish_commit(
1223 root: &Path,
1224 id: &str,
1225 session: &DurableSession,
1226 sequence: u64,
1227 snapshot: &WorkspaceSnapshot,
1228 graph: &WorkspaceGraph,
1229) -> Result<(), ServiceError> {
1230 let dir = session_directory(root, id);
1231 let tmp = dir.join(format!("commits/{sequence:020}.tmp"));
1232 let final_dir = dir.join(format!("commits/{sequence:020}"));
1233 fs::create_dir_all(&tmp).map_err(io_error)?;
1234 let snapshot = snapshot
1235 .to_canonical_json()
1236 .map_err(|error| ServiceError::new("commit_publication_failed", error.message))?;
1237 let graph = graph
1238 .to_canonical_json()
1239 .map_err(|error| ServiceError::new("commit_publication_failed", error.message))?;
1240 atomic_write(&tmp.join("workspace-snapshot.json"), &snapshot)?;
1241 atomic_write(&tmp.join("workspace-graph.json"), &graph)?;
1242 atomic_write(
1243 &tmp.join("products.json"),
1244 b"{\"schema_version\":1,\"products\":[]}\n",
1245 )?;
1246 let snapshot_id = snapshot_id(&snapshot)?;
1247 atomic_write(&tmp.join("commit.json"),format!("{{\"schema_version\":1,\"session_id\":{},\"commit_sequence\":{},\"workspace_id\":{},\"snapshot_id\":{},\"workspace_graph_schema_version\":1,\"compiler_contract\":{}}}\n",json(id),sequence,json(session.workspace_id.as_str()),json(&snapshot_id),json(session.compiler_contract.as_str())).as_bytes())?;
1248 fs::rename(&tmp, &final_dir).map_err(io_error)?;
1249 append_journal(
1250 root,
1251 id,
1252 sequence * 2,
1253 "commit_prepared",
1254 sequence,
1255 Some(&snapshot_id),
1256 )?;
1257 atomic_write(&dir.join("current"), format!("{sequence:020}\n").as_bytes())?;
1258 write_session_at(root, id, session)?;
1259 append_journal(
1260 root,
1261 id,
1262 sequence * 2 + 1,
1263 "commit_published",
1264 sequence,
1265 Some(&snapshot_id),
1266 )
1267}
1268fn append_journal(
1269 root: &Path,
1270 id: &str,
1271 sequence: u64,
1272 kind: &str,
1273 commit: u64,
1274 snapshot: Option<&str>,
1275) -> Result<(), ServiceError> {
1276 let mut file = OpenOptions::new()
1277 .create(true)
1278 .append(true)
1279 .open(session_directory(root, id).join("journal.ndjson"))
1280 .map_err(io_error)?;
1281 writeln!(file,"{{\"schema_version\":1,\"journal_sequence\":{},\"operation_id\":\"operation:service\",\"kind\":{},\"commit_sequence\":{},\"snapshot_id\":{}}}",sequence,json(kind),commit,snapshot.map_or("null".into(),json)).map_err(io_error)?;
1282 file.sync_all().map_err(io_error)
1283}
1284impl CompilerServiceHost {
1285 fn session_dir(&self, id: &str) -> PathBuf {
1286 self.root
1287 .join("sessions")
1288 .join(id.strip_prefix("session:sha256:").unwrap_or(id))
1289 }
1290}
1291fn session_directory(root: &Path, id: &str) -> PathBuf {
1292 root.join("sessions")
1293 .join(id.strip_prefix("session:sha256:").unwrap_or(id))
1294}
1295fn write_session_at(root: &Path, id: &str, session: &DurableSession) -> Result<(), ServiceError> {
1296 let config = platform::canonical_workspace_configuration_json_v1(&session.configuration)
1297 .map_err(|error| ServiceError::new("persistence_io_failed", error.message))?;
1298 let config = String::from_utf8(config).map_err(|_| {
1299 ServiceError::new(
1300 "internal_invariant_failed",
1301 "non-UTF8 canonical configuration",
1302 )
1303 })?;
1304 let dir = session_directory(root, id);
1305 fs::create_dir_all(dir.join("commits")).map_err(io_error)?;
1306 atomic_write(&dir.join("session.json"),format!("{{\"schema_version\":1,\"session_id\":{},\"workspace_id\":{},\"workspace_configuration\":{},\"compiler_contract\":{},\"state\":{},\"current_commit_sequence\":{}}}\n",json(id),json(session.workspace_id.as_str()),config.trim_end(),json(session.compiler_contract.as_str()),json(if session.closed{"closed"}else{"open"}),session.commit_sequence).as_bytes())
1307}
1308fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), ServiceError> {
1309 let tmp = path.with_extension("tmp");
1310 let mut file = File::create(&tmp).map_err(io_error)?;
1311 file.write_all(bytes).map_err(io_error)?;
1312 file.sync_all().map_err(io_error)?;
1313 fs::rename(tmp, path).map_err(io_error)
1314}
1315fn json(value: &str) -> String {
1316 serde_json::to_string(value).expect("strings serialize")
1317}
1318fn snapshot_id(bytes: &[u8]) -> Result<String, ServiceError> {
1319 let value: serde_json::Value = serde_json::from_slice(bytes)
1320 .map_err(|_| ServiceError::new("commit_publication_failed", "invalid snapshot bytes"))?;
1321 value
1322 .get("snapshot_id")
1323 .and_then(serde_json::Value::as_str)
1324 .map(str::to_owned)
1325 .ok_or_else(|| ServiceError::new("commit_publication_failed", "missing snapshot identity"))
1326}
1327fn io_error(error: std::io::Error) -> ServiceError {
1328 ServiceError::new("persistence_io_failed", error.to_string())
1329}
1330fn platform_error(error: platform::PlatformValidationError) -> ServiceError {
1331 ServiceError::new(error.code, error.message)
1332}
1333fn platform_failure(error: platform::PlatformFailure) -> ServiceError {
1334 ServiceError::new("compiler_platform_failed", error.message)
1335}
1336fn platform_serialization(error: platform::PlatformSerializationError) -> ServiceError {
1337 ServiceError::new("compiler_platform_failed", error.message)
1338}
1339fn cache_operation_error(reason: crate::persistent_cache::CacheReasonCodeV1) -> ServiceError {
1340 ServiceError::new("cache_operation_failed", reason.code())
1341}
1342fn workspace_error(error: workspace::WorkspaceErrorV1) -> ServiceError {
1343 ServiceError::new(error.code(), "workspace manifest validation failed")
1344}
1345fn watch_error(error: watch::WatchErrorV1) -> ServiceError {
1346 ServiceError::new(error.code(), "watch-session validation failed")
1347}
1348fn watch_candidate_fingerprint(
1352 request: &WorkspaceCompileRequestV1,
1353) -> Result<String, ServiceError> {
1354 let manifest = request
1355 .manifest
1356 .normalize_validate()
1357 .map_err(workspace_error)?;
1358 let mut packages = request.packages.iter().collect::<Vec<_>>();
1359 packages.sort_by(|a, b| a.package_id.cmp(&b.package_id));
1360 let mut encoded = format!("{}|{}|", manifest.identity(), request.operation_id);
1361 for package in packages {
1362 let config =
1363 platform::canonical_workspace_configuration_json_v1(&package.request.configuration)
1364 .map_err(platform_serialization)?;
1365 write!(
1366 encoded,
1367 "{}|{}|{}|{}|{:?}|{}|{:?}|{:?}|",
1368 package.package_id,
1369 package.expected_commit_sequence,
1370 package.request.candidate_snapshot.snapshot_id.as_str(),
1371 String::from_utf8_lossy(&config),
1372 package.request.mode,
1373 package.request.verify_exact_equivalence,
1374 package.request.incremental_report,
1375 package.request.cache_report,
1376 )
1377 .expect("write to string");
1378 let mut sources = package.request.sources.iter().collect::<Vec<_>>();
1379 sources.sort_by(|a, b| a.path.cmp(&b.path));
1380 for source in sources {
1381 write!(
1382 encoded,
1383 "{}\0{}\0{:?}|",
1384 source.path, source.source, source.language
1385 )
1386 .expect("write to string");
1387 }
1388 }
1389 Ok(format!("sha256:{:x}", Sha256::digest(encoded.as_bytes())))
1390}
1391fn workspace_result_identity(result: &WorkspaceBuildResultV1) -> String {
1392 let packages = result
1393 .package_results
1394 .iter()
1395 .map(|package| {
1396 format!(
1397 "{}:{}:{:?}",
1398 package.package_id, package.status, package.snapshot_id
1399 )
1400 })
1401 .collect::<Vec<_>>()
1402 .join("|");
1403 format!(
1404 "sha256:{:x}",
1405 Sha256::digest(
1406 format!(
1407 "{}|{}|{}|{}|{}|{}",
1408 result.workspace_id,
1409 result.status,
1410 result.manifest_identity,
1411 result.graph_identity,
1412 result.plan_identity,
1413 packages
1414 )
1415 .as_bytes()
1416 )
1417 )
1418}
1419
1420pub mod protocol {
1421 pub use super::{
1422 decode_frame, encode_frame, CompilerServiceDescriptor, ServiceError, ServiceOperation,
1423 ServiceResponseStatus,
1424 };
1425}
1426pub mod host {
1427 pub use super::{
1428 CompileRequest, CompileResponse, CompilerServiceHost, CompleteSource,
1429 IncrementalExecutionReportV1, IncrementalReportSelector,
1430 };
1431}
1432pub mod session_store {
1433 pub use super::CompilerServiceHost;
1434}
1435pub mod journal {
1436 pub use super::CompilerServiceHost;
1437}
1438pub mod transport {
1439 pub use super::{decode_frame, encode_frame};
1440}
1441pub mod inspection {
1442 pub use super::CompilerServiceDescriptor;
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447 use super::*;
1448
1449 fn contract() -> ContractVersion {
1450 ContractVersion::new("presolve-compiler:0.1.0-alpha.8")
1451 }
1452 fn request(
1453 configuration: WorkspaceConfiguration,
1454 sources: Vec<(&str, &str)>,
1455 report: IncrementalReportSelector,
1456 verify: bool,
1457 ) -> CompileRequest {
1458 let input = WorkspaceInput {
1459 configuration: configuration.clone(),
1460 sources: sources
1461 .iter()
1462 .map(|(path, source)| WorkspaceSource {
1463 path: (*path).into(),
1464 source: (*source).into(),
1465 language: None,
1466 })
1467 .collect(),
1468 compiler_contract: contract(),
1469 };
1470 CompileRequest {
1471 configuration,
1472 candidate_snapshot: WorkspaceSnapshot::from_input(&input).unwrap(),
1473 sources: sources
1474 .into_iter()
1475 .map(|(path, source)| CompleteSource {
1476 path: path.into(),
1477 source: source.into(),
1478 language: None,
1479 })
1480 .collect(),
1481 mode: RequestedCompilationMode::Automatic,
1482 incremental_report: report,
1483 verify_exact_equivalence: verify,
1484 cache_report: CacheReportSelector::None,
1485 }
1486 }
1487 fn root(label: &str) -> std::path::PathBuf {
1488 let root = std::env::temp_dir().join(format!(
1489 "presolve-service-l5-{label}-{}",
1490 NEXT_SERVICE.fetch_add(1, Ordering::Relaxed)
1491 ));
1492 let _ = std::fs::remove_dir_all(&root);
1493 root
1494 }
1495 fn opened_host(
1496 root: &std::path::Path,
1497 configuration: &WorkspaceConfiguration,
1498 ) -> (CompilerServiceHost, String) {
1499 let workspace = platform::derive_workspace_id_v1(configuration).unwrap();
1500 let mut host = CompilerServiceHost::start(root, contract()).unwrap();
1501 let session = host
1502 .open_session(configuration.clone(), &workspace)
1503 .unwrap();
1504 (host, session)
1505 }
1506 #[test]
1507 fn l8_explicit_watch_session_delegates_one_complete_l7_candidate() {
1508 let root = root("l8-watch");
1509 let configuration = WorkspaceConfiguration::default();
1510 let (mut host, session) = opened_host(&root, &configuration);
1511 host.create_watch_session(watch::WatchSessionConfigurationV1 {
1512 schema: watch::WATCH_SESSION_CONFIGURATION_V1_SCHEMA.into(),
1513 version: 1,
1514 watch_session_id: "watch-test".into(),
1515 workspace_id: "watch-space".into(),
1516 debounce: watch::WatchDebounceV1 {
1517 quiet_period_milliseconds: 0,
1518 maximum_delay_milliseconds: 0,
1519 },
1520 supersession_policy: "cancel_obsolete".into(),
1521 event_detail: "summary".into(),
1522 event_journal_capacity: 16,
1523 })
1524 .unwrap();
1525 let package_request = request(
1526 configuration,
1527 vec![("src/App.ts", "export class App {}")],
1528 IncrementalReportSelector::Full,
1529 true,
1530 );
1531 let workspace_request = WorkspaceCompileRequestV1 {
1532 manifest: WorkspaceManifestV1 {
1533 schema: workspace::WORKSPACE_MANIFEST_V1_SCHEMA.into(),
1534 version: 1,
1535 workspace_id: "watch-space".into(),
1536 packages: vec![workspace::WorkspacePackageDescriptorV1 {
1537 package_id: "app".into(),
1538 session_id: session,
1539 display_name: None,
1540 configuration_identity_hint: None,
1541 metadata: BTreeMap::new(),
1542 }],
1543 dependencies: vec![],
1544 policy: workspace::WorkspacePolicyV1 {
1545 failure_mode: "fail_fast".into(),
1546 execution_mode: "deterministic_serial".into(),
1547 result_detail: "summary".into(),
1548 },
1549 },
1550 packages: vec![WorkspacePackageCompileRequestV1 {
1551 package_id: "app".into(),
1552 expected_commit_sequence: 0,
1553 request: package_request,
1554 }],
1555 operation_id: "watch-operation".into(),
1556 };
1557 host.submit_watch_change_batch(
1558 WatchChangeBatchV1 {
1559 schema: watch::WATCH_CHANGE_BATCH_V1_SCHEMA.into(),
1560 version: 1,
1561 watch_session_id: "watch-test".into(),
1562 sequence: 1,
1563 observed_changes: vec![watch::ObservedChangeV1 {
1564 kind: "modified".into(),
1565 logical_path: "src/App.ts".into(),
1566 previous_logical_path: None,
1567 }],
1568 candidate: WatchCandidateV1 {
1569 candidate_id: "candidate".into(),
1570 fingerprint_hint: None,
1571 verify_exact_equivalence: true,
1572 workspace_request,
1573 },
1574 },
1575 0,
1576 )
1577 .unwrap();
1578 let report = host
1579 .run_watch_scheduler_turn("watch-test", 0)
1580 .unwrap()
1581 .unwrap();
1582 assert_eq!(report.outcome, "succeeded");
1583 let snapshot = host.inspect_watch_session("watch-test").unwrap();
1584 assert_eq!(snapshot.pending_count, 0);
1585 assert!(snapshot.last_successful_workspace_result_identity.is_some());
1586 assert!(!String::from_utf8_lossy(&read_tree(&root)).contains("export class App"));
1587 std::fs::remove_dir_all(root).unwrap();
1588 }
1589 fn read_tree(path: &Path) -> Vec<u8> {
1590 if path.is_file() {
1591 return std::fs::read(path).unwrap();
1592 }
1593 std::fs::read_dir(path)
1594 .unwrap()
1595 .flat_map(|entry| read_tree(&entry.unwrap().path()))
1596 .collect()
1597 }
1598
1599 #[test]
1600 fn frames_are_exactly_length_delimited() {
1601 let frame = encode_frame(br#"{"protocol_version":1}"#).unwrap();
1602 assert_eq!(decode_frame(&frame).unwrap(), br#"{"protocol_version":1}"#);
1603 assert!(decode_frame(b"00000000:\n").is_err());
1604 assert!(decode_frame(b"00000002:{}x\n").is_err());
1605 }
1606
1607 #[test]
1608 fn complete_candidate_commit_is_durable_without_source_persistence() {
1609 let root = std::env::temp_dir().join(format!(
1610 "presolve-service-test-{}",
1611 NEXT_SERVICE.fetch_add(1, Ordering::Relaxed)
1612 ));
1613 let configuration = WorkspaceConfiguration::default();
1614 let workspace = platform::derive_workspace_id_v1(&configuration).unwrap();
1615 let contract = ContractVersion::new("presolve-compiler:0.1.0-alpha.8");
1616 let mut host = CompilerServiceHost::start(&root, contract.clone()).unwrap();
1617 let session = host
1618 .open_session(configuration.clone(), &workspace)
1619 .unwrap();
1620 let input = WorkspaceInput {
1621 configuration: configuration.clone(),
1622 sources: vec![WorkspaceSource {
1623 path: "src/App.tsx".into(),
1624 source: "export class App {}".into(),
1625 language: None,
1626 }],
1627 compiler_contract: contract,
1628 };
1629 let snapshot = WorkspaceSnapshot::from_input(&input).unwrap();
1630 let result = host
1631 .compile(
1632 &session,
1633 0,
1634 CompileRequest {
1635 configuration,
1636 candidate_snapshot: snapshot,
1637 sources: vec![CompleteSource {
1638 path: "src/App.tsx".into(),
1639 source: "export class App {}".into(),
1640 language: None,
1641 }],
1642 mode: RequestedCompilationMode::Full,
1643 incremental_report: IncrementalReportSelector::None,
1644 verify_exact_equivalence: false,
1645 cache_report: CacheReportSelector::None,
1646 },
1647 )
1648 .unwrap();
1649 assert_eq!(result.commit_sequence, 1);
1650 let persisted = root
1651 .join("service/sessions")
1652 .join(session.strip_prefix("session:sha256:").unwrap())
1653 .join("commits/00000000000000000001");
1654 assert!(persisted.join("workspace-snapshot.json").is_file());
1655 assert!(persisted.join("workspace-graph.json").is_file());
1656 assert!(
1657 std::fs::metadata(persisted.join("commit.json"))
1658 .unwrap()
1659 .len()
1660 > 0
1661 );
1662 assert!(!String::from_utf8_lossy(&read_tree(&persisted)).contains("export class App {}"));
1663 std::fs::remove_dir_all(root).unwrap();
1664 }
1665
1666 #[test]
1667 fn l5_content_edit_reuses_validated_parse_products_and_equals_clean() {
1668 let root = root("content");
1669 let configuration = WorkspaceConfiguration::default();
1670 let (mut host, session) = opened_host(&root, &configuration);
1671 let baseline = vec![
1672 ("src/Dependency.ts", "export class Dependency {}"),
1673 (
1674 "src/App.ts",
1675 "import { Dependency } from './Dependency'; export class App {}",
1676 ),
1677 ("src/Stable.ts", "export class Stable {}"),
1678 ];
1679 let first = host
1680 .compile(
1681 &session,
1682 0,
1683 request(
1684 configuration.clone(),
1685 baseline.clone(),
1686 IncrementalReportSelector::Full,
1687 true,
1688 ),
1689 )
1690 .unwrap();
1691 assert_eq!(first.mode, "cold");
1692 let report = first.incremental_report.unwrap();
1693 assert_eq!(report.mode, IncrementalCompilationModeV1::Cold);
1694 assert_eq!(report.exact_equivalence_verified, Some(true));
1695 let candidate = vec![
1696 (
1697 "src/Dependency.ts",
1698 "export class Dependency { value = 1; }",
1699 ),
1700 (
1701 "src/App.ts",
1702 "import { Dependency } from './Dependency'; export class App {}",
1703 ),
1704 ("src/Stable.ts", "export class Stable {}"),
1705 ];
1706 let second = host
1707 .compile(
1708 &session,
1709 1,
1710 request(
1711 configuration.clone(),
1712 candidate.clone(),
1713 IncrementalReportSelector::Full,
1714 true,
1715 ),
1716 )
1717 .unwrap();
1718 let report = second.incremental_report.unwrap();
1719 assert_eq!(second.mode, "incremental");
1720 assert!(!report.reused_product_identities.is_empty());
1721 assert_eq!(report.exact_equivalence_verified, Some(true));
1722 let no_change = host
1723 .compile(
1724 &session,
1725 2,
1726 request(
1727 configuration,
1728 candidate,
1729 IncrementalReportSelector::Full,
1730 true,
1731 ),
1732 )
1733 .unwrap();
1734 assert_eq!(no_change.commit_sequence, 2);
1735 assert_eq!(no_change.mode, "no_change");
1736 assert_eq!(
1737 no_change.snapshot.to_canonical_json().unwrap(),
1738 second.snapshot.to_canonical_json().unwrap()
1739 );
1740 host.close_session(&session).unwrap();
1741 let closed = host.sessions.get(&session).unwrap();
1742 assert!(closed.closed);
1743 assert!(closed.incremental_baseline.is_none());
1744 std::fs::remove_dir_all(root).unwrap();
1745 }
1746
1747 #[test]
1748 fn l5_service_restart_has_no_durable_baseline() {
1749 let root = root("restart");
1750 let configuration = WorkspaceConfiguration::default();
1751 let baseline = vec![("src/App.ts", "export class App {}")];
1752 let (mut first, first_session) = opened_host(&root, &configuration);
1753 first
1754 .compile(
1755 &first_session,
1756 0,
1757 request(
1758 configuration.clone(),
1759 baseline.clone(),
1760 IncrementalReportSelector::None,
1761 false,
1762 ),
1763 )
1764 .unwrap();
1765 drop(first);
1766 let (mut restarted, session) = opened_host(&root, &configuration);
1767 let response = restarted
1768 .compile(
1769 &session,
1770 0,
1771 request(
1772 configuration,
1773 baseline,
1774 IncrementalReportSelector::Full,
1775 true,
1776 ),
1777 )
1778 .unwrap();
1779 assert_eq!(response.mode, "cold");
1780 assert!(response
1781 .incremental_report
1782 .unwrap()
1783 .reused_product_identities
1784 .is_empty());
1785 std::fs::remove_dir_all(root).unwrap();
1786 }
1787
1788 #[test]
1789 fn l6_persistent_complete_result_cache_hits_after_restart_without_source_text() {
1790 let root = root("l6-service");
1791 let cache_root = root.join("explicit-cache");
1792 let configuration = WorkspaceConfiguration::default();
1793 let workspace = platform::derive_workspace_id_v1(&configuration).unwrap();
1794 let sentinel = "L6_SOURCE_SENTINEL_IDENTIFIER_COMMENT_STRING";
1795 let source = format!("// {sentinel}\nexport class App {{ value = '{sentinel}'; }}");
1796 let mut first =
1797 CompilerServiceHost::start_with_cache(&root, Some(&cache_root), contract()).unwrap();
1798 let session = first
1799 .open_session(configuration.clone(), &workspace)
1800 .unwrap();
1801 let mut clean_request = request(
1802 configuration.clone(),
1803 vec![("src/App.ts", source.as_str())],
1804 IncrementalReportSelector::None,
1805 true,
1806 );
1807 clean_request.cache_report = CacheReportSelector::Full;
1808 let clean = first.compile(&session, 0, clean_request).unwrap();
1809 assert!(clean.cache_report.as_ref().unwrap().entry_published);
1810 assert!(cache_root.join("manifest.json").is_file());
1811 drop(first);
1812
1813 let mut restarted =
1814 CompilerServiceHost::start_with_cache(&root, Some(&cache_root), contract()).unwrap();
1815 let session = restarted
1816 .open_session(configuration.clone(), &workspace)
1817 .unwrap();
1818 let mut hit_request = request(
1819 configuration,
1820 vec![("src/App.ts", source.as_str())],
1821 IncrementalReportSelector::None,
1822 false,
1823 );
1824 hit_request.cache_report = CacheReportSelector::Full;
1825 let hit = restarted.compile(&session, 0, hit_request).unwrap();
1826 assert_eq!(
1827 hit.cache_report.as_ref().unwrap().outcome,
1828 CacheOutcomeV1::Hit
1829 );
1830 assert_eq!(
1831 clean.snapshot.to_canonical_json().unwrap(),
1832 hit.snapshot.to_canonical_json().unwrap()
1833 );
1834 assert_eq!(
1835 clean.graph.to_canonical_json().unwrap(),
1836 hit.graph.to_canonical_json().unwrap()
1837 );
1838 let cache_bytes = read_tree(&cache_root);
1839 assert!(!String::from_utf8_lossy(&cache_bytes).contains(sentinel));
1840 let report = restarted.inspect_cache(&cache_root).unwrap();
1841 assert_eq!(report.valid_keys.len(), 1);
1842 assert_eq!(restarted.clean_cache(&cache_root).unwrap().len(), 1);
1843 std::fs::remove_dir_all(root).unwrap();
1844 }
1845
1846 #[test]
1847 fn l6_corruption_and_disabled_cache_fall_back_to_l5() {
1848 let root = root("l6-corrupt");
1849 let cache_root = root.join("explicit-cache");
1850 let configuration = WorkspaceConfiguration::default();
1851 let workspace = platform::derive_workspace_id_v1(&configuration).unwrap();
1852 let sources = vec![("src/App.ts", "export class App {}")];
1853 let mut host =
1854 CompilerServiceHost::start_with_cache(&root, Some(&cache_root), contract()).unwrap();
1855 let session = host
1856 .open_session(configuration.clone(), &workspace)
1857 .unwrap();
1858 host.compile(
1859 &session,
1860 0,
1861 request(
1862 configuration.clone(),
1863 sources.clone(),
1864 IncrementalReportSelector::None,
1865 false,
1866 ),
1867 )
1868 .unwrap();
1869 drop(host);
1870 let payload = cache_root.join("entries");
1871 let prefix = std::fs::read_dir(&payload)
1872 .unwrap()
1873 .next()
1874 .unwrap()
1875 .unwrap()
1876 .path();
1877 let entry = std::fs::read_dir(prefix)
1878 .unwrap()
1879 .next()
1880 .unwrap()
1881 .unwrap()
1882 .path()
1883 .join("payload.bin");
1884 std::fs::write(&entry, b"corrupt").unwrap();
1885 let mut restarted =
1886 CompilerServiceHost::start_with_cache(&root, Some(&cache_root), contract()).unwrap();
1887 let session = restarted
1888 .open_session(configuration.clone(), &workspace)
1889 .unwrap();
1890 let mut corrupt_request = request(
1891 configuration.clone(),
1892 sources.clone(),
1893 IncrementalReportSelector::None,
1894 true,
1895 );
1896 corrupt_request.cache_report = CacheReportSelector::Full;
1897 let result = restarted.compile(&session, 0, corrupt_request).unwrap();
1898 assert_ne!(result.cache_report.unwrap().outcome, CacheOutcomeV1::Hit);
1899 drop(restarted);
1900 let mut disabled = CompilerServiceHost::start(&root, contract()).unwrap();
1901 let session = disabled.open_session(configuration, &workspace).unwrap();
1902 let mut request = request(
1903 WorkspaceConfiguration::default(),
1904 sources,
1905 IncrementalReportSelector::None,
1906 true,
1907 );
1908 request.cache_report = CacheReportSelector::Full;
1909 assert_eq!(
1910 disabled
1911 .compile(&session, 0, request)
1912 .unwrap()
1913 .cache_report
1914 .unwrap()
1915 .outcome,
1916 CacheOutcomeV1::Miss
1917 );
1918 std::fs::remove_dir_all(root).unwrap();
1919 }
1920
1921 #[test]
1922 fn l5_add_delete_configuration_and_malformed_baselines_clean_fallback() {
1923 let root = root("fallback");
1924 let configuration = WorkspaceConfiguration::default();
1925 let (mut host, session) = opened_host(&root, &configuration);
1926 let baseline = vec![("src/App.ts", "export class App {}")];
1927 host.compile(
1928 &session,
1929 0,
1930 request(
1931 configuration.clone(),
1932 baseline.clone(),
1933 IncrementalReportSelector::None,
1934 false,
1935 ),
1936 )
1937 .unwrap();
1938 let added = host
1939 .compile(
1940 &session,
1941 1,
1942 request(
1943 configuration.clone(),
1944 vec![
1945 ("src/App.ts", "export class App {}"),
1946 ("src/Added.ts", "export class Added {}"),
1947 ],
1948 IncrementalReportSelector::Full,
1949 true,
1950 ),
1951 )
1952 .unwrap();
1953 let report = added.incremental_report.unwrap();
1954 assert_eq!(added.mode, "clean_fallback");
1955 assert!(report
1956 .fallback_reasons
1957 .contains(&IncrementalFallbackReasonV1::SourceUniverseMembershipUnmodeled));
1958 let mut changed_configuration = configuration.clone();
1959 changed_configuration.feature_flags.push("strict".into());
1960 let config_changed = host
1961 .compile(
1962 &session,
1963 2,
1964 request(
1965 changed_configuration,
1966 vec![
1967 ("src/App.ts", "export class App {}"),
1968 ("src/Added.ts", "export class Added {}"),
1969 ],
1970 IncrementalReportSelector::Full,
1971 true,
1972 ),
1973 )
1974 .unwrap();
1975 assert!(config_changed
1976 .incremental_report
1977 .unwrap()
1978 .fallback_reasons
1979 .contains(&IncrementalFallbackReasonV1::ConfigurationChanged));
1980 host.sessions
1981 .get_mut(&session)
1982 .unwrap()
1983 .incremental_baseline
1984 .as_mut()
1985 .unwrap()
1986 .publication_identity = "malformed".into();
1987 let malformed = host
1988 .compile(
1989 &session,
1990 3,
1991 request(
1992 configuration,
1993 baseline,
1994 IncrementalReportSelector::Full,
1995 true,
1996 ),
1997 )
1998 .unwrap();
1999 assert!(malformed
2000 .incremental_report
2001 .unwrap()
2002 .fallback_reasons
2003 .contains(&IncrementalFallbackReasonV1::MalformedBaselineGraph));
2004 std::fs::remove_dir_all(root).unwrap();
2005 }
2006
2007 #[test]
2008 fn l5_failure_isolation_no_persistence_and_twenty_run_determinism() {
2009 let configuration = WorkspaceConfiguration::default();
2010 let baseline = vec![
2011 ("src/Dependency.ts", "export class Dependency {}"),
2012 (
2013 "src/App.ts",
2014 "import { Dependency } from './Dependency'; export class App {}",
2015 ),
2016 ];
2017 let candidate = vec![
2018 (
2019 "src/Dependency.ts",
2020 "export class Dependency { value = 1; }",
2021 ),
2022 (
2023 "src/App.ts",
2024 "import { Dependency } from './Dependency'; export class App {}",
2025 ),
2026 ];
2027 let mut expected_report = None;
2028 for run in 0..20 {
2029 let root = root("determinism");
2030 let (mut host, session) = opened_host(&root, &configuration);
2031 host.compile(
2032 &session,
2033 0,
2034 request(
2035 configuration.clone(),
2036 baseline.clone(),
2037 IncrementalReportSelector::None,
2038 false,
2039 ),
2040 )
2041 .unwrap();
2042 let mut invalid = request(
2043 configuration.clone(),
2044 baseline.clone(),
2045 IncrementalReportSelector::None,
2046 false,
2047 );
2048 invalid.sources = vec![
2049 CompleteSource {
2050 path: "src/App.ts".into(),
2051 source: "export class App {}".into(),
2052 language: None,
2053 },
2054 CompleteSource {
2055 path: "src/App.ts".into(),
2056 source: "export class Duplicate {}".into(),
2057 language: None,
2058 },
2059 ];
2060 assert!(host.compile(&session, 1, invalid).is_err());
2061 let response = host
2062 .compile(
2063 &session,
2064 1,
2065 request(
2066 configuration.clone(),
2067 candidate.clone(),
2068 IncrementalReportSelector::Full,
2069 true,
2070 ),
2071 )
2072 .unwrap();
2073 let report = response.incremental_report.unwrap().to_canonical_json();
2074 if let Some(expected) = &expected_report {
2075 assert_eq!(&report, expected, "determinism run {run}");
2076 } else {
2077 expected_report = Some(report);
2078 }
2079 let persisted = read_tree(&root.join("service/sessions"));
2080 assert!(!String::from_utf8_lossy(&persisted).contains("value = 1"));
2081 std::fs::remove_dir_all(root).unwrap();
2082 }
2083 }
2084}