Skip to main content

presolve_compiler/
service.rs

1//! Local, durable compiler-service host (Phase L4).
2//!
3//! The host accepts complete request-owned workspace inputs and delegates every
4//! compilation to `platform::CompilerSessionState`; it never reads workspace
5//! files or persists source text.
6
7#![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    /// Optional L5 inspection data. `None` preserves the L4 response surface.
110    pub incremental_report: IncrementalReportSelector,
111    /// Explicit test-only proof mode. It never enables production semantics.
112    pub verify_exact_equivalence: bool,
113    /// Optional L6 cache telemetry. `None` preserves the L4/L5 response surface.
114    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    /// # Panics
137    ///
138    /// Panics only if serializing an owned Rust string fails, which `serde_json`
139    /// guarantees for strings.
140    #[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/// Transient L8 input.  The complete workspace request is never placed in a
214/// snapshot, event journal, or durable service state.
215#[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    /// At most one unexecuted complete replacement request.  This is strictly
234    /// process-local and is released on coalesce, completion, or stop.
235    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    /// Closes a live session and releases its non-durable L5 baseline before
420    /// recording the existing L4 durable closed-state marker.
421    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    /// Creates a process-local L8 watch session.  This has no persistence and
459    /// intentionally performs no compilation or input discovery.
460    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    /// Accepts an exact complete replacement candidate. The caller, not the
489    /// compiler service, owns all observation and source acquisition.
490    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            // Replacing this drops the prior complete candidate and its authored
543            // source text immediately; the state machine itself is source-free.
544            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    /// Runs one caller-controlled deterministic scheduler turn. This is an
559    /// internal service executor hook, not a wall-clock timer or public API.
560    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        // The sole active request is owned on this stack only; it is released as
584        // soon as L7 returns. L7 remains the only compiler/workspace executor.
585        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            // L7 failure is represented in the watch event/report while prior
602            // watch success remains untouched; callers can inspect the session.
603            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        // A restored L6 baseline deliberately has no parser products. Do not
925        // pretend it can support L5 durable partial reuse.
926        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                    // A fallback is intentionally a clean canonical L3 compile.
1007                    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}
1348/// Candidate identity is derived from the complete normalized L7 request. It
1349/// binds all package snapshot identities (which bind exact source universes),
1350/// configuration identities, request modes, and expected publication state.
1351fn 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}