Skip to main content

presolve_compiler/
tooling_reader.rs

1//! L11-B byte-only readers for the explicitly approved L3 tooling products.
2//!
3//! A reader negotiates first, then delegates to an existing strict canonical
4//! decoder. It never opens a path, parses source, invokes a compiler session,
5//! or widens L3--L8 persistence.
6
7#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
8
9use std::fmt;
10
11use crate::platform::{
12    decode_workspace_graph_json_v1, decode_workspace_snapshot_json_v1, WorkspaceGraph,
13    WorkspaceSnapshot,
14};
15use crate::tooling_products::{
16    decode_tooling_artifact_graph_v1, decode_tooling_build_trace_v1,
17    decode_tooling_compile_cost_report_v1, ToolingArtifactGraphV1, ToolingBuildTraceV1,
18    ToolingCompileCostReportV1, ARTIFACT_GRAPH_TOOLING_SCHEMA_V1, BUILD_TRACE_TOOLING_SCHEMA_V1,
19    COMPILE_COST_TOOLING_SCHEMA_V1,
20};
21use crate::tooling_schema::{
22    decode_tooling_schema_negotiation_request_v1, negotiate_tooling_schema_v1,
23};
24
25pub const WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1: &str = "presolve.workspace-snapshot";
26pub const WORKSPACE_GRAPH_TOOLING_SCHEMA_V1: &str = "presolve.workspace-graph";
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ToolingProductV1 {
30    WorkspaceSnapshot(WorkspaceSnapshot),
31    WorkspaceGraph(WorkspaceGraph),
32    BuildTrace(ToolingBuildTraceV1),
33    CompileCostReport(ToolingCompileCostReportV1),
34    ArtifactGraph(ToolingArtifactGraphV1),
35}
36
37impl ToolingProductV1 {
38    #[must_use]
39    pub const fn schema(&self) -> &'static str {
40        match self {
41            Self::WorkspaceSnapshot(_) => WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
42            Self::WorkspaceGraph(_) => WORKSPACE_GRAPH_TOOLING_SCHEMA_V1,
43            Self::BuildTrace(_) => BUILD_TRACE_TOOLING_SCHEMA_V1,
44            Self::CompileCostReport(_) => COMPILE_COST_TOOLING_SCHEMA_V1,
45            Self::ArtifactGraph(_) => ARTIFACT_GRAPH_TOOLING_SCHEMA_V1,
46        }
47    }
48
49    #[must_use]
50    pub const fn version(&self) -> u32 {
51        1
52    }
53
54    #[must_use]
55    pub fn snapshot_id(&self) -> &str {
56        match self {
57            Self::WorkspaceSnapshot(snapshot) => snapshot.snapshot_id.as_str(),
58            Self::WorkspaceGraph(graph) => graph.snapshot_id.as_str(),
59            Self::BuildTrace(trace) => trace.snapshot_id.as_deref().unwrap_or(""),
60            Self::CompileCostReport(report) => report.build_id.as_str(),
61            Self::ArtifactGraph(graph) => graph.build_id.as_str(),
62        }
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ToolingProductReadErrorV1 {
68    pub code: &'static str,
69    pub message: String,
70}
71
72impl fmt::Display for ToolingProductReadErrorV1 {
73    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(output, "{}: {}", self.code, self.message)
75    }
76}
77
78impl std::error::Error for ToolingProductReadErrorV1 {}
79
80/// Reads one caller-supplied immutable L3 product after strict L10
81/// negotiation. This function owns no filesystem access: its caller supplies
82/// exactly one byte slice.
83pub fn read_tooling_product_v1(
84    schema: &str,
85    versions: &[u32],
86    bytes: &[u8],
87) -> Result<ToolingProductV1, ToolingProductReadErrorV1> {
88    let negotiation = serde_json::to_vec(&serde_json::json!({
89        "schema": schema,
90        "versions": versions,
91    }))
92    .expect("tooling negotiation request serializes");
93    let request = decode_tooling_schema_negotiation_request_v1(&negotiation).map_err(|error| {
94        ToolingProductReadErrorV1 {
95            code: "L11T001",
96            message: format!("schema negotiation rejected: {error}"),
97        }
98    })?;
99    let accepted =
100        negotiate_tooling_schema_v1(&request).map_err(|error| ToolingProductReadErrorV1 {
101            code: "L11T001",
102            message: format!("schema negotiation rejected: {error}"),
103        })?;
104    if accepted.accepted_version != 1 {
105        return Err(ToolingProductReadErrorV1 {
106            code: "L11T006",
107            message: "accepted tooling schema version does not match the reader".into(),
108        });
109    }
110
111    match accepted.schema.as_str() {
112        WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1 => decode_workspace_snapshot_json_v1(bytes)
113            .map(ToolingProductV1::WorkspaceSnapshot)
114            .map_err(|error| ToolingProductReadErrorV1 {
115                code: "L11T003",
116                message: format!("workspace snapshot fails strict canonical validation: {error:?}"),
117            }),
118        WORKSPACE_GRAPH_TOOLING_SCHEMA_V1 => decode_workspace_graph_json_v1(bytes)
119            .map(ToolingProductV1::WorkspaceGraph)
120            .map_err(|error| ToolingProductReadErrorV1 {
121                code: "L11T003",
122                message: format!("workspace graph fails strict canonical validation: {error:?}"),
123            }),
124        BUILD_TRACE_TOOLING_SCHEMA_V1 => decode_tooling_build_trace_v1(bytes)
125            .map(ToolingProductV1::BuildTrace)
126            .map_err(|error| ToolingProductReadErrorV1 {
127                code: "L11T007",
128                message: format!("build trace fails strict validation: {error:?}"),
129            }),
130        COMPILE_COST_TOOLING_SCHEMA_V1 => decode_tooling_compile_cost_report_v1(bytes)
131            .map(ToolingProductV1::CompileCostReport)
132            .map_err(|error| ToolingProductReadErrorV1 {
133                code: "L11T008",
134                message: format!("compile cost report fails strict validation: {error:?}"),
135            }),
136        ARTIFACT_GRAPH_TOOLING_SCHEMA_V1 => decode_tooling_artifact_graph_v1(bytes)
137            .map(ToolingProductV1::ArtifactGraph)
138            .map_err(|error| ToolingProductReadErrorV1 {
139                code: "L11T010",
140                message: format!("artifact graph fails strict validation: {error:?}"),
141            }),
142        _ => Err(ToolingProductReadErrorV1 {
143            code: "L11T005",
144            message: "no accepted public reader exists for the negotiated schema".into(),
145        }),
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::platform::{
153        derive_workspace_id_v1, CacheLimits, CancellationToken, CompilationOutcome,
154        CompileWorkspaceRequest, CompilerSessionState, RequestedCompilationMode, WorkspaceInput,
155        WorkspaceSource,
156    };
157
158    fn canonical_products() -> (Vec<u8>, Vec<u8>) {
159        let workspace = WorkspaceInput::new(vec![WorkspaceSource {
160            path: "src/ToolingFixture.ts".into(),
161            source: "export const toolingFixture = 1;\n".into(),
162            language: None,
163        }]);
164        let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
165        let mut session = CompilerSessionState::new(
166            workspace_id,
167            workspace.compiler_contract.clone(),
168            CacheLimits::default(),
169        );
170        let CompilationOutcome::Committed(result) =
171            session.compile_workspace(CompileWorkspaceRequest {
172                workspace,
173                mode: RequestedCompilationMode::Full,
174                cancellation: CancellationToken::new(),
175            })
176        else {
177            panic!("tooling reader fixture compilation must commit");
178        };
179        let snapshot = result.snapshot.to_canonical_json().unwrap();
180        let graph = result.graph.to_canonical_json().unwrap();
181        assert!(!String::from_utf8_lossy(&snapshot).contains("toolingFixture"));
182        assert!(!String::from_utf8_lossy(&graph).contains("toolingFixture"));
183        (snapshot, graph)
184    }
185
186    #[test]
187    fn l11b_reads_only_strict_canonical_l3_product_fixtures() {
188        let (snapshot_bytes, graph_bytes) = canonical_products();
189        let snapshot =
190            read_tooling_product_v1(WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1, &[1], &snapshot_bytes)
191                .unwrap();
192        let graph =
193            read_tooling_product_v1(WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, &[2, 1], &graph_bytes)
194                .unwrap();
195        assert_eq!(snapshot.schema(), WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1);
196        assert_eq!(graph.schema(), WORKSPACE_GRAPH_TOOLING_SCHEMA_V1);
197        assert_eq!(snapshot.version(), 1);
198        assert_eq!(graph.version(), 1);
199        assert_eq!(snapshot.snapshot_id(), graph.snapshot_id());
200    }
201
202    #[test]
203    fn l11b_rejects_unavailable_products_and_noncanonical_bytes() {
204        let (snapshot_bytes, _) = canonical_products();
205        let error = read_tooling_product_v1("presolve.unknown", &[1], &snapshot_bytes).unwrap_err();
206        assert_eq!(error.code, "L11T001");
207        for schema in ["presolve.workspace-configuration", "presolve.watch-event"] {
208            let error = read_tooling_product_v1(schema, &[1], &snapshot_bytes).unwrap_err();
209            assert_eq!(error.code, "L11T005");
210        }
211
212        let mut noncanonical = snapshot_bytes;
213        noncanonical.pop();
214        let error =
215            read_tooling_product_v1(WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1, &[1], &noncanonical)
216                .unwrap_err();
217        assert_eq!(error.code, "L11T003");
218    }
219
220    #[test]
221    fn l11b_reader_order_and_version_validation_are_deterministic() {
222        let (snapshot_bytes, graph_bytes) = canonical_products();
223        let forward = [
224            (
225                WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
226                snapshot_bytes.as_slice(),
227            ),
228            (WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, graph_bytes.as_slice()),
229        ]
230        .map(|(schema, bytes)| read_tooling_product_v1(schema, &[1], bytes).unwrap());
231        let reverse = [
232            (WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, graph_bytes.as_slice()),
233            (
234                WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
235                snapshot_bytes.as_slice(),
236            ),
237        ]
238        .map(|(schema, bytes)| read_tooling_product_v1(schema, &[1], bytes).unwrap());
239        assert_eq!(forward[0].snapshot_id(), reverse[1].snapshot_id());
240        assert_eq!(forward[1].snapshot_id(), reverse[0].snapshot_id());
241
242        let error =
243            read_tooling_product_v1(WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, &[1, 1], &graph_bytes)
244                .unwrap_err();
245        assert_eq!(error.code, "L11T001");
246    }
247}