Skip to main content

presolve_cli/
compilation_commands.rs

1//! L9-C complete-request adapter for the frozen L4 compiler service.
2//!
3//! Callers own source acquisition. This module receives exact complete source
4//! inputs, derives the canonical candidate snapshot, and delegates once to
5//! L4. It does not read paths, parse source, or recreate compiler products.
6
7#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
8
9use std::path::Path;
10
11use presolve_compiler::persistent_cache::CacheReportSelector;
12use presolve_compiler::platform::{
13    derive_workspace_id_v1, ContractVersion, RequestedCompilationMode, WorkspaceConfiguration,
14    WorkspaceInput, WorkspaceSnapshot, WorkspaceSource,
15};
16use presolve_compiler::service::{
17    CompileRequest, CompilerServiceHost, CompleteSource, IncrementalReportSelector, ServiceError,
18};
19
20/// One exact caller-provided source input. Its path is an L3 logical path,
21/// rather than an authority to read a host filesystem path.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct CliSourceInputV1 {
24    pub logical_path: String,
25    pub content: String,
26}
27
28/// Complete L9-C project candidate. There is deliberately no path loader in
29/// this API: the later command layer supplies already-authorized inputs.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CliCompilationCandidateV1 {
32    pub configuration: WorkspaceConfiguration,
33    pub sources: Vec<CliSourceInputV1>,
34    pub verify_clean_equivalence: bool,
35    pub report: IncrementalReportSelector,
36    pub cache_report: CacheReportSelector,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct CliCompilationResultV1 {
41    pub workspace_id: String,
42    pub commit_sequence: u64,
43    pub snapshot_id: String,
44    pub graph_snapshot_id: String,
45    pub mode: String,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct CliCompilationErrorV1 {
50    pub code: &'static str,
51    pub message: String,
52}
53
54fn compiler_contract() -> ContractVersion {
55    ContractVersion::new(format!("presolve-compiler:{}", env!("CARGO_PKG_VERSION")))
56}
57
58fn service_error(error: ServiceError) -> CliCompilationErrorV1 {
59    CliCompilationErrorV1 {
60        code: error.code,
61        message: error.message,
62    }
63}
64
65fn candidate_snapshot(
66    candidate: &CliCompilationCandidateV1,
67    contract: &ContractVersion,
68) -> Result<WorkspaceSnapshot, CliCompilationErrorV1> {
69    WorkspaceSnapshot::from_input(&WorkspaceInput {
70        configuration: candidate.configuration.clone(),
71        sources: candidate
72            .sources
73            .iter()
74            .map(|source| WorkspaceSource {
75                path: source.logical_path.clone(),
76                source: source.content.clone(),
77                language: None,
78            })
79            .collect(),
80        compiler_contract: contract.clone(),
81    })
82    .map_err(|error| CliCompilationErrorV1 {
83        code: "L9C201_INVALID_COMPLETE_CANDIDATE",
84        message: error.message,
85    })
86}
87
88/// Delegates a complete candidate to a fresh local L4 service lifecycle.
89///
90/// This is intentionally the only L9-C compilation path. It never calls the
91/// legacy CLI parser or code-generation helpers.
92pub fn compile_complete_candidate_v1(
93    service_root: &Path,
94    candidate: CliCompilationCandidateV1,
95) -> Result<CliCompilationResultV1, CliCompilationErrorV1> {
96    let contract = compiler_contract();
97    let workspace_id = derive_workspace_id_v1(&candidate.configuration).map_err(|error| {
98        CliCompilationErrorV1 {
99            code: "L9C202_INVALID_CONFIGURATION",
100            message: error.message,
101        }
102    })?;
103    let snapshot = candidate_snapshot(&candidate, &contract)?;
104    let mut service = CompilerServiceHost::start(service_root, contract).map_err(service_error)?;
105    let session_id = service
106        .open_session(candidate.configuration.clone(), &workspace_id)
107        .map_err(service_error)?;
108    let response = service
109        .compile(
110            &session_id,
111            0,
112            CompileRequest {
113                configuration: candidate.configuration,
114                candidate_snapshot: snapshot,
115                sources: candidate
116                    .sources
117                    .into_iter()
118                    .map(|source| CompleteSource {
119                        path: source.logical_path,
120                        source: source.content,
121                        language: None,
122                    })
123                    .collect(),
124                mode: RequestedCompilationMode::Automatic,
125                incremental_report: candidate.report,
126                verify_exact_equivalence: candidate.verify_clean_equivalence,
127                cache_report: candidate.cache_report,
128            },
129        )
130        .map_err(service_error)?;
131    service.close_session(&session_id).map_err(service_error)?;
132    Ok(CliCompilationResultV1 {
133        workspace_id: workspace_id.to_string(),
134        commit_sequence: response.commit_sequence,
135        snapshot_id: response.snapshot.snapshot_id.to_string(),
136        graph_snapshot_id: response.graph.snapshot_id.to_string(),
137        mode: response.mode,
138    })
139}
140
141#[cfg(test)]
142mod tests {
143    use std::sync::atomic::{AtomicU64, Ordering};
144
145    use super::*;
146
147    static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
148
149    fn candidate() -> CliCompilationCandidateV1 {
150        CliCompilationCandidateV1 {
151            configuration: WorkspaceConfiguration::default(),
152            sources: vec![CliSourceInputV1 {
153                logical_path: "src/main.ts".into(),
154                content: "export const L9_C_SOURCE_SENTINEL = 1;\n".into(),
155            }],
156            verify_clean_equivalence: true,
157            report: IncrementalReportSelector::Summary,
158            cache_report: CacheReportSelector::Summary,
159        }
160    }
161
162    #[test]
163    fn l9c_delegates_complete_caller_owned_source_input_to_l4() {
164        let root = std::env::temp_dir().join(format!(
165            "presolve-l9c-{}",
166            NEXT_ROOT.fetch_add(1, Ordering::Relaxed)
167        ));
168        let result = compile_complete_candidate_v1(&root, candidate()).unwrap();
169        assert_eq!(result.commit_sequence, 1);
170        assert!(result.snapshot_id.starts_with("snapshot:"));
171        assert_eq!(result.snapshot_id, result.graph_snapshot_id);
172        let session = std::fs::read_dir(root.join("service").join("sessions"))
173            .unwrap()
174            .next()
175            .unwrap()
176            .unwrap()
177            .path()
178            .join("session.json");
179        let durable = std::fs::read_to_string(session).unwrap();
180        assert!(!durable.contains("L9_C_SOURCE_SENTINEL"));
181        std::fs::remove_dir_all(root).unwrap();
182    }
183}